Repository navigation
Releases: cheezy/stride-pi
Release list
v1.21.0 — identifier-form completion URLs route the diff correctly, and a release runbook
Fixed — an identifier-form completion URL no longer routes the diff through a stale claim cache (D309)
taskIdFromCommand read only a numeric id, while the server also resolves /api/tasks/<identifier>/complete. A completion URL such as /api/tasks/W2185/complete therefore resolved no id and the changed_files upload fell back to the claim-derived env cache, which still holds the previous task when the claim response was concealed — so the diff landed on that task with a 2xx and no error. The regex now accepts a numeric id or a G/W/D identifier, still tail-anchored and still returning "" on any other segment, so the numeric path is unchanged. The closed class keeps the captured id safe to interpolate into the PUT path, and the env-cache fallback stays for ids that are not literal in the command.
Added — what this extension is, and is not, exposed to by a hidden API response (W2185, W2187)
ADR-002 gains an addendum, and the README a canon-anchored paragraph, recording the split verdict on the fleet's stdout-preservation curl guard: the diff's content is not exposed, because the snapshot comes from git and the upload is this extension's own fetch(), but its destination was. Since D309 both say the identifier case is closed and name the two shapes that still reach the cache — a shell-expanded id and a literal segment in neither accepted form.
Added — a release runbook for this repository (W2173)
RELEASE.md records how this repository is released, as its own history shows it: which file holds the version, the changelog's shape (and that it has changed over time), that there is no catalog to sync, and the one-line check for whether the changelog's top heading is already tagged. Documentation only.
v1.20.0
Added — a back-reference beside every anchored rule (W2137)
Canon entry edit-site-back-reference requires prose beside every governed-rule statement naming the canon by path and the entry id, and the drift check implements it as a property, so an anchor standing alone is reported rather than passing. Four paragraphs close this extension's five anchored statements: the verdict-note pair in extensions/subagent-dispatch/agents/stride-task-reviewer.md — this repository's reviewer is nested there rather than at a top-level agents/ path — and the decision matrix, its ordering, the review ceiling and the reason_code vocabulary in skills/stride-workflow/SKILL.md.
Each says rewording is free and changing the claim is not, and closes by naming what the checker actually inspects: the anchor, never the sentence. Its decision-matrix-authority and row-precedence anchors are three lines apart — the tightest pair in the fleet — so one paragraph names both and says which prose answers to which.
v1.19.0 — the two-round cap, the cosmetic class, and dispatch_count telemetry
Ports the three review-convergence rules stride shipped in its 1.74.0 (goal G430), each adapted to Pi's own surfaces. Two of the three answer differently here than in the sibling ports, and for the same underlying reason: this port has an executable self-check, so "stated, not enforced" had to be argued from what that check can actually observe rather than from its absence.
A two-round ceiling on review, and a terminal state to reach when it binds (W2167)
Review is capped at two rounds, the second verifying rather than re-reviewing. A round is an invocation of the reviewer — by dispatch_agent or inline as the stride-task-reviewer skill — whose response yielded a first fenced ```json block that parsed into structured. Deliberately not "a dispatch": both invocation paths report dispatched: true, so a crashed one would otherwise burn a round.
The cap is prose, and the reason is specific to this port rather than inherited. The parsed block carries no round number, the reviewer schema is pinned as a mirror of the canonical one, and no executable surface here can observe that a round happened — dispatch_agent receives no task identifier, and the hook bridge sees only the claim, complete and mark_reviewed calls.
The terminal state is defined here, not ported. This port had no review_blocked status and no "stop without completing" vocabulary, and an exploratory session had rated that exact absence a Major stranding path in a sibling port. It is now four concrete actions, and a session run against this port confirmed all four are executable from where the agent stands.
An optional cosmetic class on reviewer findings, refused mechanically (W2168)
Entries in issues[] may carry an optional cosmetic boolean: presentational only, and valid only on a minor finding that is not category: "security".
This port enforces that rather than only stating it, and the divergence from its siblings is the point. They carry the rule as prose partly because they have no executable self-check; this port has one. The deeper reason they gave — that an assert would read a value the agent set from its own memory — is specific to a round number. A cosmetic shape pin reads severity, category and the flag's type, all reviewer-authored and already present in structured, so here it can refuse for real. The asserts were executed against fixtures before being written in, and all three claimed refusals fire.
schema_version moves 1.4 → 1.7 across the declarations, with the gap recorded rather than papered over: this port carries neither 1.5's nested considerations[] nor 1.6's behaviour_test_matrix, and the declarations now name both absences.
dispatch_count review-cost telemetry, with its limits rebuilt (W2169)
The reviewer entry of workflow_steps may carry an optional dispatch_count: how many times that step's subagent was invoked, an inline run included, since Step 5 reports both paths as dispatched: true. It counts invocations, not rounds — one that crashed still spent its tokens. Omitting it stays valid and no seventh step name is added.
The six limits ship inline. Four could not be ported as written:
- Limit (2) inverts. stride warns against dividing duration by count because a security specialist, an exploratory session and a hardening step all fold into
duration_ms. None of those three exists here, so nothing folds in and the division really is a mean per invocation — still not a per-round figure, because a crashed invocation costs a dispatch and consumes no round. - Limit (5) drops stride's recovery half. This port writes no per-round artifact, the count lives only in session memory, and a structured round field is forbidden rather than merely missing.
- Limit (6) claims more than stride's, not less. The executable self-check exists; it simply reads the reviewer's parsed block and never touches
workflow_steps. Nothing here validates the key, client or server, so the guard falls to the first consumer that reads it. - Limit (3) names both readings of a
dispatched: falsereviewer entry — a decision-matrix skip, where the missing figures cost nothing because nothing ran, andran_inline, where the review did run and the entry records none of what it cost.
Verified before release: check-port-canon.sh reports stride-pi clean on all nine canon rules; npm test 265 pass, 0 fail.
v1.18.0 — loop-state evidence, and an advisory continuation that is not a gate
Added — loop-state evidence on completion, and an advisory continuation that says plainly it is not a gate (W2150, W2151)
Stride's Claude Code plugin refuses to let a session end while a claimable task remains: its Stop hook exits 2 and the harness honours the refusal. This port cannot do that, and the reason is not a gap someone forgot to fill — it is visible in the type signatures. turn_end and agent_end register as ExtensionHandler<TurnEndEvent> with one type parameter, while tool_call and before_agent_start get two; the on() overloads that accept a returned object are exactly the two-parameter ones. An end-of-turn handler is therefore structurally incapable of returning a refusal — whatever it computes is discarded. So this release ships the two things that are available: the evidence, and an advisory.
The completion record (W2150). A successful /complete now writes .stride/.loop-state.json, and any claim clears it. Four keys, in the shell writer's order: identifier, needs_review, completed_at, session_id. This is a fleet contract, not a port-local artifact — stride's stride-stop-gate.sh and its PowerShell twin read exactly these keys out of a shared checkout — so needs_review is written as the literal JSON boolean rather than a string. That detail is the whole point of the contract: the gate tests (.needs_review | type) == "boolean", so a stringified "false" would silently defeat terminal-state detection rather than erroring, and the failure would look exactly like a session with nothing to hold open. The write is atomic (a temp file inside .stride/, then a rename, so a reader never sees a half-file) and never fatal to the completion that triggered it; the clear is an unlink rather than an empty write, because {} parses and would read as a record with no usable verdict.
Sourcing the record is two-tier, and the second tier is guarded. Tier 1 is the intercepted call's own output. Tier 2 is the canonical response file, admitted only when .hooks is an array and .data.id matches the id the command routed on — because that file survives across calls, so on a truncated or failed completion it still holds the previous claim payload, which carries both fields and would record a completion that never happened. The regression test for this deliberately uses a truncated body rather than a 422: a 422 body is valid JSON and overwrites the canonical file, so it cannot reach the trap at all.
The advisory continuation (W2151). A before_agent_start handler reads that record and, when it shows a completion no claim has followed, injects one message naming the claimable task from GET /api/tasks/next. It advises the next turn; it does not gate the previous one. By the time it can speak the turn that should have claimed is over, and if nobody prompts again it never fires. An injected message is context, not a veto — the model remains free to do something else.
Because Pi supplies no stop_hook_active equivalent on this path, the only bound is the port's own counter at .stride/.pi-advisory-continuations, keyed on the completed task and defaulting to two injections per unfollowed completion. STRIDE_PI_ADVISORY_MAX overrides it, unsigned decimal only — and that validation is load-bearing rather than tidy: Number("off") is NaN, count + 1 > NaN is false, so an unvalidated override would make the advisory unbounded, reached by someone trying to switch it off. The counter is written before the message is returned, since an injection the port cannot count is one it cannot bound.
What reaches the prompt, and what is refused. Only the claimable task's identifier — never a token, a URL, or a response body. It is checked against ^[A-Za-z0-9_-]{1,32}$ and refused rather than sanitised when it does not match, because a scrubbed value is still a value a remote server chose and it lands in text a model reads. A session id of unknown — the writer's sentinel for "identity could not be established" — is likewise refused on both sides; the shell gate's time-window fallback for that case is deliberately not copied, because it exists only where the bash side cannot learn the session id at all, and Pi always can.
Credential handling tightened beyond the shell twin. The new .stride_auth.md reader selects the production **API Token:** line by literal substring so a **Local API Token:** line cannot be picked up instead, and the API base is now re-parsed with a real URL parser rather than trusted as a regex match. Two properties of the bare match made it unsafe as a token destination: @ fell outside the character class, so https://evil.example@www.stridelikeaboss.com matched only https://evil.example and the bearer token would have gone to the attacker's host — a different host than any conformant parser resolves — and plaintext http was accepted, putting a token on the wire. Userinfo is now refused, plaintext is refused except to loopback so local development keeps working, and the origin is used. The shell resolver in stride-hook.sh still has both properties; this port diverges deliberately and says so in the module header.
ADR-002 records the decision. A new evidence subsection (d) proves the result-type asymmetry with file and line references, and a closing Addendum states the three consequences a reader must not mistake for a gate: it cannot prevent a stop, the model may ignore it, and nothing bounds it but us. The tool_call veto was considered as a lever and left alone — blocking the tool calls of a turn that is trying to finish is far more invasive and deserves its own decision.
Outstanding. The end-to-end confirmation that the injection appears on the next turn requires a running Pi session and was not performed.
v1.17.0 — row precedence, the complexity fallback row, and the reason_code vocabulary
Added — Step 3 row precedence, the complexity fallback row, and the reason_code skip vocabulary (W2112, closing D253 and porting D239)
Two rules the fleet canon records for every port had no text behind them in this one, and the drift check reported all four in-scope rules missing here.
Row precedence and the fallback row. More than one row of the Step 3 decision matrix can describe the same task — a defect of medium complexity matches the complexity row and the type row at once — and nothing said which one governed, leaving the collision D232 had just cleared out of the surrounding prose alive inside the rows themselves. Step 3 gains a Row Precedence subsection stating the reading order: Branch A's row, then small, 0-1 key_files whatever the task's type, then Defect type ahead of the bare complexity rows, then the complexity row itself, then the fallback. That order follows the printed rows except in one place — Defect type prints sixth and is settled third. The matrix also gains a closing Complexity absent or unrecognised row (Decompose Skip, Explore YES, Plan YES, Review YES) for a task arriving with no complexity or an unfamiliar one, which until now matched no row whatsoever; the precedence text confines that row to exactly that case and rules out using it to break a tie. The stride-subagent-workflow mirror gains the same row in its own Run/Skip vocabulary, since that table is required to agree with Step 3 row for row — the order itself is stated once, in Step 3, and the mirror points at it. Putting the single-file row above the type row was deliberate: reversed, Explore and Review would flip to YES for every small single-file defect, contradicting Branch B, so this ordering resolves the ambiguity without moving any task onto a different route (D221, D232).
The reason_code skip vocabulary (D239). A workflow_steps entry with dispatched: false may now carry an optional reason_code beside its prose reason, never in place of it — the code is what a compliance breakdown can group on; the sentence is for the person reading the task. The Per-Step Schema table gains the key, and a new vocabulary subsection gives the six accepted values: decision_matrix_skip, ran_inline, hook_body_empty, subsumed_by_task_spec, folded_into_prior_step, matrix_deviation. The set is closed — anything outside it is refused with a 422 — while omitting the key stays valid, so a payload written before this field existed validates unchanged. matrix_deviation is the one value that records non-compliance, and it exists so that a step the matrix required and nobody ran cannot be filed as a sanctioned skip.
Canon anchors. The four in-scope rules — the failed-verdict note rule, the sole-decision-point rule, row precedence, and the reason_code vocabulary — each gain the canon marker comment beside this port's own statement of the rule, so the fleet drift check can tell "carried here" apart from "never checked". The markers only mark; no rule text was reworded to match another port. The verdict-note marker sits beside the Verdict-note rule in this runtime's nested reviewer prompt at extensions/subagent-dispatch/agents/stride-task-reviewer.md, which enumerates this port's four section verdicts and gains no fifth: there is no behaviour_test_matrix verdict on this runtime, and the canon records that divergence rather than smoothing it over.
Documentation-only and producer-side: no completion field becomes required, and no server behaviour changes.
v1.16.0
Fixed — the failed-verdict note rule the server already enforces (D240)
This port's task-reviewer prompt described note as optional on every section verdict. The completion API has required it on a "failed" verdict since D231, and enforces that unconditionally — independently of the strict_completion_validation flag — so an agent on this runtime could emit a note-less failed verdict that its own prompt endorsed and be rejected with a 422. The rejection is self-describing and recoverable, so nothing was broken; every such completion simply paid an avoidable round trip.
The prompt now states that on a "failed" section verdict note is REQUIRED and must name the specific violation or gap in at least 20 non-whitespace characters, carries the anti-placeholder prohibition (no stub, TODO, empty string, or bare restatement of the status), and directs that an empty note means the verdict is wrong rather than that the note is unnecessary. note stays optional on "passed" and "not_assessed", so the ordinary empty-section case gains no friction.
Producer-side only: the server-side check in Kanban.Tasks.CompletionValidation.ReviewContract is unchanged, and no port was accommodated by weakening it.
Fixed — planner precedence: the decision matrix is the sole decision point (D232, propagating D221)
This port carried the same ambiguity D221 fixed in the canonical plugin: the stride-workflow Step 3 decision matrix row small, 2+ key_files says Plan = Skip, while Branch C prose independently said "If medium+ OR 3+ key_files OR 3+ acceptance criteria lines: Outline your implementation approach" — two separately-satisfiable planner triggers with no stated precedence. The same conflict pattern existed for the Explore and Review columns (stride-workflow Step 5, stride-subagent-workflow Phases 1–3 plus its role quick-table, stride-completing-tasks' pre-completion review items, and this port's unique stride-task-explorer/stride-task-reviewer skill activation lines), plus drifted narrower "medium+"-only restatements in the flowcharts and quick-reference cards. Measured consequence in canonical: two runners on identically-shaped tasks resolved the collision differently and wrote different skip reasons into workflow_steps telemetry.
The fix mirrors canonical's D221 resolution: the Step 3 matrix now states it is the sole decision point for its columns, and every restatement reads its matrix column with "Read the column; do not re-derive the condition here (D221)" instead of re-deriving a condition. A small task carrying 3+ key_files or 3+ acceptance-criteria lines remains a mis-labelling signal to record in completion notes, never an independent planner trigger. Resolved toward the matrix (Plan = Skip for small, 2+ key_files), so no planner dispatch is added to the most common task shape.
Recorded verification grep (should return only row definitions, D221 history, and matrix-agreeing glosses — never a rule that could fire independently of the matrix):
grep -rniE "if medium|medium\+ OR|medium or large, OR|3\+ (key_files|criteria|acceptance)|2\+ key_files" --include="*.md" skills/ extensions/
v1.15.0
Fixed — three stale workflow step cross-references (D176)
This port numbers Code Review as Step 5, Execute Hooks as Step 6, Complete as Step 7 and Post-Completion as Step 8. Three references disagreed: the orchestrator entry point read "Step 7-8" (now Step 6-7), the code-review prerequisite read Step 6 (now Step 5), and AGENTS.md pointed at "Step 7+9" — a step this port does not have. That last one now names Step 8 for the after_goal procedure and Step 6 for the GOAL_* env matrix.
References citing "Extracting the structured review block" by name carry no step number and were deliberately left alone — that is why this port never had the seven-citation form of this defect.
Added — behaviour_test_matrix in the Step 5 self-review checklist (W1949), and this release also carries the previously unreleased D151 enrichment-envelope fix.
Also in this push: the release-record note for this repo's three unreleased tags (W1943).
v1.14.0 — top-level agent_name on create payloads
Ports the canonical stride plugin's W1684 change (released as stride v1.37.0) into the Pi extension — the last of the six-plugin fleet, after stride-codex (v1.25.0), stride-copilot (v2.26.0), stride-gemini (v1.36.0), and stride-opencode (v1.28.0).
Added — every documented create payload carries a top-level agent_name (W1694)
stride-creating-tasks, stride-creating-goals, and both decomposer docs — skills/stride-task-decomposer/SKILL.md and extensions/subagent-dispatch/agents/stride-task-decomposer.md, which carry the contract in parallel — now document a top-level agent_name on every create request: beside the task root key for POST /api/tasks and beside the goals root key for POST /api/tasks/batch, set to the exact same plain agent name the extension already sends as agent_name on claim and complete ("Pi", never the ai_agent:<model> token form).
Per-task created_by_agent is forgotten in practice and cannot be backfilled (PATCH rejects it), so tasks lost their attribution permanently and the /agents feed rendered them with a ? avatar. The root-level param is the always-sent fallback the server reads.
Both creation skills gain the full five-step server resolution order (explicit created_by_agent → token ai_agent:<model> → top-level agent_name → token's last agent name → unset), an agent_name row in their field tables, and an explicit note that agent_name is display metadata only — never an authorization signal.
Fixed — stride-creating-tasks documented the single-create body without its task root key
The skill's complete example was a bare task object, but POST /api/tasks requires a {"task": {...}} envelope and returns 422 Missing 'task' key without it. A new Request Envelope section shows the wrapper with agent_name as its top-level sibling, and the Quick Reference heading now names the block as the value of the task key rather than the request body; the single-goal format in both decomposer docs is corrected the same way. The extension inherited this defect from the canonical plugin, where W1684 fixed it.
Scope
Documentation-only. The extension's TypeScript builds no create payload — the hook bridge handles curl matching, changed_files capture/PUT, and the after_goal PATCH, while the agent itself issues the create request. skills/stride-workflow/SKILL.md references the create endpoints in prose only and embeds no payload shape, so it is unchanged.
Testing
npm test (node --test) — 213 pass, 0 fail, unchanged: no .ts file was touched.
Backward compatibility
Fully backward compatible, and safe to ship ahead of the server. No .ts, hook, .stride.md, env-var, or .stride_auth.md change. Unknown top-level keys are ignored by older servers. created_by_agent guidance is unchanged and still highest precedence — the new param is a fallback, never a replacement.
v1.13.0 — post-section TASK_BASE_REF, trust guard, committed-range (D142/D147)
Ports all three of the canonical stride plugin's D142 base-ref / snapshot fixes (stride v1.36.0) into the Pi hook bridge. TASK_BASE_REF was captured at claim time — before runHook ran the ## before_doing section's git pull — so the after_doing changed_files diff spanned commits pulled from another clone (D132/W1678), and the W1529 dirty-baseline filter dropped committed task work (D137). Per ADR-002 all new logic lives in the pure, unit-tested modules; index.ts only wires them.
Fixed
- D132 (reorder + trust marker).
resolveClaimEnvCachenow persists task identity only and strips any inheritedTASK_BASE_REF/TASK_BASE_REF_TRUSTED; a new pureresolveFinalizeBeforeDoingEnvwrites the base + trust marker, called fromindex.tsafterrunHookreturns (regardless of exit code), re-recording the dirty baseline against the post-pull tree. - D132 (trust guard). New exported
resolveSnapshotBase(baseRef, cwd, trusted)— the three reference rules with the trusted-marker gate on rule 3, a loudconsole.errorrecompute, and a no-origin passthrough.finalizeAfterDoingandselfHealChangedFilesUploadresolve it once per task window and persist it as abase=line in.stride-diff-upload-state, reusing it so an after_doinggit pushcannot recompute a correct base toHEADand empty the snapshot. - D137 (committed range).
captureChangedFilesoverrides the dirty-baseline exclusion for any path in thebase..HEADcommitted range: committed range = task work.
Testing
npm test (node --test) grows from 195 to 213 tests, all passing. New coverage in the pure modules: resolveSnapshotBase over a two-clone bare-origin cross-pull fixture, the committed-range override, the base= round-trip, readFinalizerEnv trust-marker parsing, the inverted env resolvers, and two finalizeAfterDoing integration tests. The new tests reference resolveSnapshotBase (absent pre-fix), so they fail by construction against old code.
Backward compatibility
Backward-compatible and additive. The TASK_BASE_REF_TRUSTED cache key and base= state line are tolerated when absent. No marketplace — the extension installs via a github: ref pin.
stride-pi 1.12.1 — changed_files upload fix (D127 + W1658)
changed_files upload reliability fix: ports the two Claude Code hook changes (D127 + W1658) that stop completed review tasks from landing with an empty changed_files array. The hook was uploading the per-file diff to the task id from the env cache, which goes stale when the claim response is hidden from the hook — so the diff was PUT to the previous task and the current task's changed_files stayed empty, silently. This is a patch bump (1.12.0 → 1.12.1): no wire shapes change and the upload target resolution is now derived from the authoritative /complete URL.
Fixed — target the changed_files upload by the /complete URL id (D127)
extensions/hook-bridge/curl-matcher.ts,changed-files.ts(W1671) — NewtaskIdFromCommand()parses the bare numeric id from a/completeor/mark_reviewedcommand URL (mirrorstask_id_from_commandinstride/hooks/stride-hook.sh; pure string parse, no network call, non-numeric segments rejected).finalizeAfterDoingandselfHealChangedFilesUploadnow resolve the upload target astaskIdFromCommand(command) || opts.taskId, so a stale env-cacheTASK_IDcan no longer misroute the diff; the env id remains the fallback only on the claim path (whose URL carries no id). 8 unit tests + stale-env targeting integration tests for both finalize and self-heal.
Fixed — fail loud on a terminal changed_files upload failure (W1658)
extensions/hook-bridge/changed-files.ts(W1672) — When thebefore_reviewself-heal's last-retry PUT returns non-2xx, the hook now emits a distinctCHANGED_FILES UPLOAD UNRESOLVED for task <id> (HTTP <code>)message to stderr and appendsunresolved=yesto.stride-diff-upload-state, so a definitively-lost diff is queryable rather than silent. Fail-soft: the completion is never vetoed. A later successful (2xx) PUT overwrites the state file and self-clears the mark; a legitimately-empty diff that PUTs 2xx takes the success path. Only the id + HTTP code are recorded — never the bearer token. 3 tests. Thenode --test extensions/hook-bridge/*.test.tssuite grew to 195 passing tests.
Backward compatibility
No hook-bridge wire shapes changed — the claim/complete/changed_files payloads and the structured hook-result JSON are identical. The change is confined to how the upload target id is resolved (URL-derived, env-cache fallback) and an added terminal fail-loud signal.