/project — Multi-Spec Project Surface

SkillDev tools

Register, inspect, and drive multi-spec projects via the instar /projects API. Twelve subcommands cover the full Phase 1 surface — create / status / next / advance / drift / run-round / halt / ack / resume / abandon / accept-partial / claim-ownership.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the /project — Multi-Spec Project Surface skill

What this skill tells your AI

The instructions your AI receives, as published by jkheadley/instar in .claude/skills/instar-project/SKILL.md and read by ahel’s review.

Spec: docs/specs/PROJECT-SCOPE-SPEC.md § Phase 1.7. A project bundles many feature initiatives into rounds. The dashboard Projects tab, the session-start digest, and the compaction-recovery hook keep them visible. This skill is the user-invocable surface for inspecting and driving them.


Setup — read auth + port once

AUTH=$(python3 -c "import json; print(json.load(open('.instar/config.json')).get('authToken',''))" 2>/dev/null)
PORT=$(python3 -c "import json; print(json.load(open('.instar/config.json')).get('port',4040))" 2>/dev/null)

Every endpoint except /health requires Authorization: Bearer $AUTH.


/project create <plan-doc-path>

Register a new project from a plan-doc markdown file. The plan-doc schema is PlanDocParser's contract (spec § Phase 1.6).

Pre-flight first (no rate-limit cost):

curl -sS -X POST -H "Authorization: Bearer $AUTH" \
  -H "Content-Type: application/json" \
  -d "{\"planDocPath\": \"$(realpath PLAN_DOC.md)\"}" \
  "http://localhost:${PORT}/projects/validate"

Returns 200 {ok, project, children, errors}. Iterate until ok:true.

Then create:

curl -sS -X POST -H "Authorization: Bearer $AUTH" \
  -H "Content-Type: application/json" \
  -d "{\"planDocPath\": \"$(realpath PLAN_DOC.md)\"}" \
  "http://localhost:${PORT}/projects"
  • 201{project, children}.
  • 400 — validation failed; surface each error.
  • 409 — slug already exists.
  • 429 — rate-limited (5 creates/hour per auth token); body includes windowEnds.

/project status [id]

No id — list all projects:

curl -sS -H "Authorization: Bearer $AUTH" "http://localhost:${PORT}/projects"

Render to user: id, title, per-round status summary (e.g. 2/4 complete, 1 in-progress, 1 pending).

With id — fetch the project plus its child items:

curl -sS -H "Authorization: Bearer $AUTH" "http://localhost:${PORT}/projects/<id>"

Returns {project, children}. Render: title, status, version, round-by-round breakdown with each item's pipelineStage, plus any blockers or awaitingUser reason.

The GET also runs a lazy merged-state reconciler — children at pipelineStage: 'building' with a mergeCommitOid are re-verified against origin/main (debounced 6h, capped at 3 per call). Pass ?reconcile=false to skip.


/project next [id]

Returns the next action the agent should take on this project.

curl -sS -H "Authorization: Bearer $AUTH" \
  "http://localhost:${PORT}/projects/<id>/next"
  • 200 {action, params, skillCommand}action is one of await-user-approval, ack-required, resolve-conflict, accept-partial, run-spec-converge, run-drift-check, repair-merge-evidence, start-round. repair-merge-evidence means a historical item says merged but lacks the PR/commit/check evidence needed to support that conclusion; advance it to merged again with the PR artifact to re-attest it without re-running the work. skillCommand is a suggested /project ... (or /spec-converge) invocation. params.roundIndex, params.itemIds, params.status are the round context.
  • 204 — every round is complete.
  • 404 — id is not a project.

Surface the suggested skillCommand to the user. Do NOT auto-run a mutating skill (run-round, ack) without explicit user consent — read-only suggestions (run-spec-converge, run-drift-check) are fine to act on.


/project advance <id> <itemId> <targetStage>

Manually transition one child item between pipeline stages. The server-side validator (StageTransitionValidator) checks the artifact behind each transition — outline → spec-drafted requires a markdown spec file at docs/specs/, spec-converged → approved requires approved: true in frontmatter, approved → building needs a TaskFlow record id, building → merged confirms the PR is MERGED and its mergeCommit.oid is reachable from origin/main.

A same-stage merged → merged request is allowed only for a historical merged row whose evidence is incomplete. It runs the full merge validator again and atomically attaches the verified evidence; it does not redo the feature or fabricate missing fields.

PROJECT_ID=...
ITEM_ID=...
TARGET_STAGE=spec-drafted   # or spec-converged, approved, building, merged, regressed, skipped
PROJECT_VERSION=...   # read from /projects/<id>

curl -sS -X POST -H "Authorization: Bearer $AUTH" \
  -H "Content-Type: application/json" \
  -H "If-Match: ${PROJECT_VERSION}" \
  -d "{\"itemId\": \"${ITEM_ID}\", \"targetStage\": \"${TARGET_STAGE}\", \"artifact\": {\"specPath\": \"docs/specs/foo.md\"}}" \
  "http://localhost:${PORT}/projects/${PROJECT_ID}/advance"
  • 200 {item, project} — transition applied.
  • 409 — version mismatch (re-GET the project, retry) OR artifact validation failed (body includes code + reason).
  • 404 — item not under this project.
  • 428If-Match header missing.

The artifact body shape depends on the target stage; surface the validator's reason field on rejection so the user knows what's missing.


/project drift <id> <roundIndex> <specPath>

Run the drift checker for one round. The verdict is a signal, not an authority — it tells the agent whether the spec premise still holds against the current state of referenced files.

curl -sS -X POST -H "Authorization: Bearer $AUTH" \
  -H "Content-Type: application/json" \
  -d "{\"roundIndex\": 0, \"specPath\": \"docs/specs/foo.md\", \"referencedFiles\": [\"src/foo.ts\", \"src/bar.ts\"]}" \
  "http://localhost:${PORT}/projects/<id>/drift-check"
  • 200 {verdict, projectId, roundIndex}verdict.status is no-drift, minor-drift, premise-violated, or manual-review-required. On premise-violated, the verdict carries byte-range citations — surface them to the user.
  • 409 — another drift-check for this project is already in flight (mutex-guarded; protects the spend ledger and LLM bill).
  • 503 — no IntelligenceProvider configured (no LLM available). The verdict is unavailable; the round can still proceed but is flying blind on drift.

/project run-round <id> [roundIndex]

Manual trigger to start a round. Calls ProjectRoundRunner.preflight (lock, drift, owner, ack-gap) and, on accept, sets autoAdvanceAt = now so the poller fires the executor on its next tick (≤60s). Does NOT spawn the autonomous child directly — that path goes through the poller to keep one fire path through one lock.

curl -sS -X POST -H "Authorization: Bearer $AUTH" \
  -H "Content-Type: application/json" \
  -d "{\"roundIndex\": 0}" \
  "http://localhost:${PORT}/projects/<id>/run-round"
  • 200 {id, roundIndex, scheduledAt, version} — preflight passed, round scheduled. The autonomous child will start within ~60s.
  • 409 {error, code, reason} — preflight rejected; the reason text says what's missing (drift verdict, ack, owner, lock). Surface the reason verbatim to the user — these are actionable.
  • 404 — round index out of range.
  • 503ProjectRoundRunner not wired (server has no intelligence provider, or the runner failed to start at boot).

/project halt <id> [reason]

Immediately cancel the active round. Writes haltedAt to the round, sets project.status = 'halted', signals the autonomous child via SIGTERM (5s grace, then SIGKILL), releases the round-runner lock. Worktrees are retained for inspection.

curl -sS -X POST -H "Authorization: Bearer $AUTH" \
  -H "Content-Type: application/json" \
  -d "{\"reason\": \"spec drift detected upstream\"}" \
  "http://localhost:${PORT}/projects/<id>/halt"
  • 200 {id, roundIndex, version} — round halted.
  • 409 — no halt-able round (project has no in-progress round).
  • 503ProjectRoundRunner not wired.

Halt is idempotent; repeated calls return 200 against the same round.


/project ack <id> [roundIndex]

Record the user's acknowledgment for the first auto-advance of a project (firstLaunchAckAt) and reset the unacknowledged-advance counter. Required by the runner's preflight: a project's first round cannot fire without firstLaunchAckAt; after two unacknowledged auto-advances the project is paused until acked.

curl -sS -X POST -H "Authorization: Bearer $AUTH" \
  -H "Content-Type: application/json" \
  -d "{\"forRoundIndex\": 0}" \
  "http://localhost:${PORT}/projects/<id>/ack"
  • 200 {id, firstLaunchAckAt, lastAckedRoundIndex, unacknowledgedAdvanceCount, version}.
  • 404 — project not found.
  • 503ProjectRoundRunner not wired.

Ack is also accepted via Telegram reply OR the dashboard Ack button — this route is the explicit-API path. /project approve <id> is documented as an alias because the structured /projects/:id/next payload returns skillCommand: "/project approve ..." for the await-user-approval action; both invocations call the same ack endpoint.


/project resume <id> [roundIndex] [--force]

Resume a halted round. Clears haltedAt/haltReason and schedules the round for the poller. For rounds at status failed with resumeAttempts >= 3 (spec's 3-attempt cap), --force is required and the attempt counter is reset.

# Normal resume
curl -sS -X POST -H "Authorization: Bearer $AUTH" \
  -H "Content-Type: application/json" \
  -d "{\"roundIndex\": 0}" \
  "http://localhost:${PORT}/projects/<id>/resume"

# Force-resume a failed round at the cap
curl -sS -X POST -H "Authorization: Bearer $AUTH" \
  -H "Content-Type: application/json" \
  -d "{\"roundIndex\": 0, \"force\": true}" \
  "http://localhost:${PORT}/projects/<id>/resume"
  • 200 {id, roundIndex, scheduledAt, forced, version} — round scheduled to re-fire.
  • 409 — round is neither halted nor failed; or it's at the resume cap and force was not set.
  • 404 — round index out of range.

Resume restores project.status from halted/abandoned back to active so the poller considers it again.


/project abandon <id>

Archive a halted project. Sets project.status = 'abandoned', clears any future autoAdvanceAt on remaining rounds, leaves each child's pipelineStage untouched. Idempotent. Refuses (409) if any round is currently in-progress — halt first.

curl -sS -X POST -H "Authorization: Bearer $AUTH" \
  "http://localhost:${PORT}/projects/<id>/abandon"
  • 200 {id, status, version} — project abandoned. Body includes alreadyAbandoned: true on idempotent repeat.
  • 409 — there's an in-progress round; halt it first.

/project accept-partial <id> <roundIndex> <reason> <skippedBy>

Close a partially-complete round (some items merged, others skipped). Records the skip reason in the project's audit log and advances lastAckedRoundIndex so the next round can fire. The skipped items get pipelineStage = 'skipped'.

curl -sS -X POST -H "Authorization: Bearer $AUTH" \
  -H "Content-Type: application/json" \
  -d "{\"roundIndex\": 0, \"reason\": \"upstream dependency blocked\", \"skippedBy\": \"justin\"}" \
  "http://localhost:${PORT}/projects/<id>/accept-partial"
  • 200 {id, skippedItemIds, version}.
  • 400reason or skippedBy missing.
  • 404 — project or round not found.
  • 503ProjectRoundRunner not wired.

/project claim-ownership <id>

Multi-machine ownership transfer. The current machine writes its machineId as ownerMachineId on the project record. The auto-advance poller only fires rounds whose owner matches the running machine, so this is the gate for moving a project between machines.

PROJECT_VERSION=...   # read from /projects/<id>

curl -sS -X POST -H "Authorization: Bearer $AUTH" \
  -H "Content-Type: application/json" \
  -H "If-Match: ${PROJECT_VERSION}" \
  -d "{}" \
  "http://localhost:${PORT}/projects/<id>/claim-ownership"

Pass {"force": true} to override a current owner whose heartbeat is still fresh — by default the claim is refused with 409 in that case.

  • 200 {id, ownerMachineId, previousOwner, version} — claim recorded. Body includes alreadyOwned: true if the caller already owns it.
  • 409 — current owner is alive (heartbeat fresh) and force was not set; OR If-Match version mismatch.
  • 428If-Match header missing.
  • 503 — machine heartbeat not configured.

Per spec § Phase 1.12: after claim, the caller must commit-and-push the claim before acting on it, then wait 60s for git-sync to converge. This route only records the change; the wait-and-converge is the caller's responsibility.


Session-start integration

Active projects (top 5 by lastTouchedAt) show up automatically at session start and after context compaction. The data comes from .instar/projects-digest.cache, written by the server every time a project mutates. No need to invoke /project status to see what's open — the digest is already in your context.

If the cache is missing the hook emits:

Active projects: state unavailable — run /project status when ready.

That's the cue to call /project status (no id) once.


Conversational rendering — talk, don't dump

These commands return JSON. Render results to the user as narrative, not raw output. For /project status: a sentence per project with round progress. For /project next: state what the next action is and why, then offer to take it. For errors: surface the reason text verbatim — those are written for users, not developers.

Never paste a curl command in a user-facing reply.

Signals

GitHub stars
79
Forks
18
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
instar-project
Source
github.com/jkheadley/instar