@opengsd/gsd-core 1.19.0 → 1.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/agents/gsd-code-fixer.compact.md +5 -5
- package/agents/gsd-code-fixer.md +9 -4
- package/agents/gsd-code-reviewer.compact.md +49 -8
- package/agents/gsd-code-reviewer.md +53 -14
- package/agents/gsd-executor.md +8 -4
- package/commands/gsd/update.md +5 -2
- package/gsd-core/bin/gsd-tools.cjs +73 -23
- package/gsd-core/bin/lib/capability-lifecycle.cjs +1 -12
- package/gsd-core/bin/lib/capability-registry.cjs +284 -93
- package/gsd-core/bin/lib/capability-validator.cjs +15 -0
- package/gsd-core/bin/lib/commands.cjs +27 -17
- package/gsd-core/bin/lib/config-loader.cjs +3 -3
- package/gsd-core/bin/lib/core-utils.cjs +11 -0
- package/gsd-core/bin/lib/frontmatter.cjs +21 -3
- package/gsd-core/bin/lib/gate-api-coverage-verify-pre.cjs +2 -2
- package/gsd-core/bin/lib/gate-context-drift.cjs +1 -1
- package/gsd-core/bin/lib/gate-phase-context.cjs +7 -8
- package/gsd-core/bin/lib/gate-schema-drift.cjs +1 -1
- package/gsd-core/bin/lib/git-info-exclude.cjs +1 -6
- package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +13 -9
- package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +9 -7
- package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +3 -3
- package/gsd-core/bin/lib/init.cjs +180 -67
- package/gsd-core/bin/lib/io.cjs +6 -0
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +34 -1
- package/gsd-core/bin/lib/markdown-table.cjs +23 -0
- package/gsd-core/bin/lib/mcp-server.cjs +8 -5
- package/gsd-core/bin/lib/milestone.cjs +18 -50
- package/gsd-core/bin/lib/phase-entry-grammar.cjs +119 -0
- package/gsd-core/bin/lib/phase-id-display.cjs +2 -0
- package/gsd-core/bin/lib/phase-id.cjs +609 -35
- package/gsd-core/bin/lib/phase-lifecycle.cjs +9 -74
- package/gsd-core/bin/lib/phase-locator.cjs +328 -19
- package/gsd-core/bin/lib/phase.cjs +2990 -666
- package/gsd-core/bin/lib/planning-document.cjs +26 -5
- package/gsd-core/bin/lib/planning-inspect.cjs +27 -35
- package/gsd-core/bin/lib/planning-snapshot.cjs +12 -7
- package/gsd-core/bin/lib/planning-workspace.cjs +3 -3
- package/gsd-core/bin/lib/read-only-git-env.cjs +23 -2
- package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +2 -2
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +22 -0
- package/gsd-core/bin/lib/review-lane-invocation.cjs +125 -18
- package/gsd-core/bin/lib/reviewer-step-dispatch.cjs +11 -9
- package/gsd-core/bin/lib/roadmap-parser.cjs +730 -144
- package/gsd-core/bin/lib/roadmap-progress.cjs +136 -0
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +8 -5
- package/gsd-core/bin/lib/roadmap.cjs +358 -106
- package/gsd-core/bin/lib/shell-command-projection.cjs +36 -9
- package/gsd-core/bin/lib/smart-entry.cjs +22 -11
- package/gsd-core/bin/lib/state-contract.cjs +11 -7
- package/gsd-core/bin/lib/state-document.cjs +332 -23
- package/gsd-core/bin/lib/state-transition.cjs +39 -10
- package/gsd-core/bin/lib/state.cjs +498 -179
- package/gsd-core/bin/lib/template.cjs +2 -2
- package/gsd-core/bin/lib/validate.cjs +3 -2
- package/gsd-core/bin/lib/vendor/re2js.cjs +863 -577
- package/gsd-core/bin/lib/vendor/re2js.d.cts +284 -219
- package/gsd-core/bin/lib/verify.cjs +2 -2
- package/gsd-core/bin/lib/workstream-inventory.cjs +26 -20
- package/gsd-core/bin/verify-reapply-patches.cjs +411 -10
- package/gsd-core/references/cicd-review-patterns.md +45 -0
- package/gsd-core/references/necessity-review.md +48 -0
- package/gsd-core/workflows/code-review-fix.md +46 -5
- package/gsd-core/workflows/code-review.md +3 -0
- package/gsd-core/workflows/execute-phase.md +1 -1
- package/gsd-core/workflows/execute-plan.md +1 -1
- package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +13 -9
- package/gsd-core/workflows/plan-phase.md +1 -1
- package/gsd-core/workflows/pr-branch.md +2 -0
- package/gsd-core/workflows/reapply-patches.md +104 -0
- package/gsd-core/workflows/review.md +54 -141
- package/gsd-core/workflows/secure-phase.md +1 -1
- package/gsd-core/workflows/settings-advanced.md +57 -9
- package/gsd-core/workflows/ship.md +12 -0
- package/hooks/dist/gsd-graphify-update.sh +6 -0
- package/hooks/dist/gsd-statusline.js +3 -2
- package/hooks/dist/gsd-windsurf-pre-write.js +4 -4
- package/hooks/dist/gsd-workflow-guard.js +2 -1
- package/hooks/dist/gsd-worktree-path-guard.js +4 -4
- package/hooks/dist/lib/git-probe.js +16 -0
- package/hooks/gsd-graphify-update.sh +6 -0
- package/hooks/gsd-statusline.js +3 -2
- package/hooks/gsd-windsurf-pre-write.js +4 -4
- package/hooks/gsd-workflow-guard.js +2 -1
- package/hooks/gsd-worktree-path-guard.js +4 -4
- package/hooks/lib/git-probe.js +16 -0
- package/package.json +3 -3
- package/pi/package-agents/gsd-code-fixer.md +9 -4
- package/pi/package-agents/gsd-code-reviewer.md +53 -14
- package/pi/package-agents/gsd-executor.md +8 -4
- package/pi/package-agents/manifest.json +3 -3
- package/pi/package-skills/gsd-update/SKILL.md +4 -1
- package/scripts/ci-next-health.cjs +246 -23
- package/scripts/docs-guard-registry.cjs +2 -0
- package/scripts/gen-features.cjs +14 -2
- package/scripts/lib/drift-scan.cjs +27 -0
- package/scripts/lib/macos-conformance-tier.generated.cjs +6 -0
- package/scripts/lib/platform-conformance-tier.generated.cjs +11 -0
- package/scripts/lint-phase-enumeration-drift.cjs +8 -4
- package/scripts/lint-roadmap-progress-drift.cjs +421 -0
- package/scripts/mutation-matrix.cjs +23 -1
- package/scripts/sync-next-version.cjs +38 -6
- package/skills/gsd-update/SKILL.md +5 -2
- package/vscode/package.json +1 -1
|
@@ -31,7 +31,7 @@ command -v kimi >/dev/null 2>&1 && echo "kimi-code:available" || echo "kimi-code
|
|
|
31
31
|
# hosts and defaults come from each lane's descriptor
|
|
32
32
|
gsd_run review-lane availability || { echo "WARNING: review-lane availability failed or is unsupported by this gsd-tools; treating local lanes as missing" >&2; printf 'ollama:missing\nlm_studio:missing\nllama_cpp:missing\n'; }
|
|
33
33
|
|
|
34
|
-
# jq prerequisite (#2589). The config/model
|
|
34
|
+
# jq prerequisite (#2589). The config/model lookups in this workflow no
|
|
35
35
|
# longer need jq — they use the native --raw/--pick flags. But the lanes listed
|
|
36
36
|
# under "jq-dependent reviewer lanes" below parse structured JSON that gsd-tools
|
|
37
37
|
# does not emit (OpenAI-compatible /v1/chat/completions responses, opencode's
|
|
@@ -245,7 +245,7 @@ Output your review in markdown format.
|
|
|
245
245
|
|
|
246
246
|
Write to a temp file: `{run_dir}/gsd-review-prompt.md`
|
|
247
247
|
|
|
248
|
-
|
|
248
|
+
Append the plan coverage manifest to the prompt:
|
|
249
249
|
|
|
250
250
|
```bash
|
|
251
251
|
# #2962: zsh aborts the block on an unmatched for-list glob (nomatch); bash passes it through. nullglob both.
|
|
@@ -253,38 +253,21 @@ shopt -s nullglob 2>/dev/null; setopt NULL_GLOB 2>/dev/null
|
|
|
253
253
|
|
|
254
254
|
RUN_DIR="{run_dir}" # from gather_context
|
|
255
255
|
|
|
256
|
-
# Write individual section files for per-reviewer budget trimming
|
|
257
|
-
# These are always written so reviewers with a budget can invoke prompt-budget
|
|
258
|
-
cp "$INSTRUCTIONS_BLOCK_FILE" "${RUN_DIR}/gsd-review-instructions.md"
|
|
259
|
-
cp "$ROADMAP_SECTION_FILE" "${RUN_DIR}/gsd-review-roadmap.md"
|
|
260
|
-
|
|
261
|
-
# Plan files: copy each PLAN.md to a predictable path named after its source
|
|
262
|
-
# plan id (#3959: a bare padded index discards provenance — the budget tool's
|
|
263
|
-
# per-plan `### <file>` header then renders a run-dir artifact name no reviewer
|
|
264
|
-
# or consensus step can resolve. The plan id keeps the gsd-review-plan-*.md glob
|
|
265
|
-
# prepare_trimmed_prompt_for_reviewer consumes.)
|
|
266
|
-
for PLAN_FILE in "${PHASE_DIR}"/*-PLAN.md; do
|
|
267
|
-
PLAN_BASENAME=$(basename "$PLAN_FILE")
|
|
268
|
-
PLAN_ID="${PLAN_BASENAME%-PLAN.md}"
|
|
269
|
-
cp "$PLAN_FILE" "${RUN_DIR}/gsd-review-plan-${PLAN_ID}.md"
|
|
270
|
-
done
|
|
271
|
-
|
|
272
256
|
# #3301: plan coverage manifest — tell reviewers exactly which plan ids exist and
|
|
273
257
|
# how many there are, so a review that silently covers 6 of 7 plans is no longer
|
|
274
258
|
# indistinguishable from one that covers all 7. The id is the plan file's own
|
|
275
259
|
# basename with the `-PLAN.md` suffix stripped (e.g. `12.6-01-PLAN.md` ->
|
|
276
260
|
# `12.6-01`) — NOT the `plan:` frontmatter key, which holds only the bare
|
|
277
261
|
# in-phase sequence number ("01") and can never reconstruct the phase-qualified
|
|
278
|
-
# id reviewers need to cite. The filename is guaranteed present for every
|
|
279
|
-
#
|
|
262
|
+
# id reviewers need to cite. The filename is guaranteed present for every plan,
|
|
263
|
+
# so no plan is ever dropped from the manifest for lacking a key.
|
|
280
264
|
# Count and bullets are both derived directly from the glob loop below, never
|
|
281
265
|
# from re-splitting an accumulated string: bash word-splits an unquoted `$VAR`
|
|
282
266
|
# on IFS by default, but zsh does not (no `setopt SH_WORD_SPLIT` here), so a
|
|
283
267
|
# prior `PLAN_IDS="$PLAN_IDS $id"` + `for x in $PLAN_IDS` round-trip silently
|
|
284
268
|
# collapsed every id onto one iteration under zsh whenever there were 2+ plans
|
|
285
|
-
# (gsd-core#4099). Direct glob iteration (`for f in "${PHASE_DIR}"/*-PLAN.md
|
|
286
|
-
#
|
|
287
|
-
# never needs word-splitting at all.
|
|
269
|
+
# (gsd-core#4099). Direct glob iteration (`for f in "${PHASE_DIR}"/*-PLAN.md`)
|
|
270
|
+
# is identical under both shells, so this never needs word-splitting at all.
|
|
288
271
|
PLAN_COUNT=0
|
|
289
272
|
PLAN_ID_BULLETS=""
|
|
290
273
|
for PLAN_FILE in "${PHASE_DIR}"/*-PLAN.md; do
|
|
@@ -295,10 +278,8 @@ for PLAN_FILE in "${PHASE_DIR}"/*-PLAN.md; do
|
|
|
295
278
|
"
|
|
296
279
|
done
|
|
297
280
|
|
|
298
|
-
#
|
|
299
|
-
#
|
|
300
|
-
# above) — a manifest matching either would be picked up as a report or as a
|
|
301
|
-
# plan to review.
|
|
281
|
+
# Dot-prefixed so the `gsd-review-*.md` glob over RUN_DIR (reviewer reports,
|
|
282
|
+
# invoke_reviewers) can never pick it up as a report.
|
|
302
283
|
{
|
|
303
284
|
echo ""
|
|
304
285
|
echo "## Plan Coverage Manifest"
|
|
@@ -309,33 +290,11 @@ done
|
|
|
309
290
|
printf '%s' "$PLAN_ID_BULLETS"
|
|
310
291
|
} > "${RUN_DIR}/.plans-manifest.md"
|
|
311
292
|
|
|
312
|
-
#
|
|
313
|
-
if [ -f ".planning/PROJECT.md" ]; then
|
|
314
|
-
cp .planning/PROJECT.md "${RUN_DIR}/gsd-review-project.md"
|
|
315
|
-
fi
|
|
316
|
-
_CTX=( "${PHASE_DIR}"/*-CONTEXT.md )
|
|
317
|
-
if [ ${#_CTX[@]} -gt 0 ]; then
|
|
318
|
-
cat "${_CTX[@]}" > "${RUN_DIR}/gsd-review-context.md"
|
|
319
|
-
fi
|
|
320
|
-
_RESEARCH=( "${PHASE_DIR}"/*-RESEARCH.md )
|
|
321
|
-
if [ ${#_RESEARCH[@]} -gt 0 ]; then
|
|
322
|
-
cat "${_RESEARCH[@]}" > "${RUN_DIR}/gsd-review-research.md"
|
|
323
|
-
fi
|
|
324
|
-
if [ -f ".planning/REQUIREMENTS.md" ]; then
|
|
325
|
-
cp .planning/REQUIREMENTS.md "${RUN_DIR}/gsd-review-requirements.md"
|
|
326
|
-
fi
|
|
327
|
-
|
|
328
|
-
# #3301: append the manifest to BOTH files reviewers actually read — the
|
|
329
|
-
# per-lane budget-trimmed instructions file (descriptor lanes get
|
|
330
|
-
# `--instructions-file`) and the full combined prompt (combined-prompt lanes
|
|
331
|
-
# read the whole file). The `instructions` fragment is in prompt-budget's
|
|
332
|
-
# `minimumFor` floor set and is never trimmed, so this survives per-lane
|
|
333
|
-
# budget trimming intact.
|
|
334
|
-
cat "${RUN_DIR}/.plans-manifest.md" >> "${RUN_DIR}/gsd-review-instructions.md"
|
|
293
|
+
# #3301: append the manifest to the one prompt file every lane reads.
|
|
335
294
|
cat "${RUN_DIR}/.plans-manifest.md" >> "${RUN_DIR}/gsd-review-prompt.md"
|
|
336
295
|
```
|
|
337
296
|
|
|
338
|
-
Note: `
|
|
297
|
+
Note: `PHASE_DIR` comes from prompt assembly; `RUN_DIR` is the run-scoped dir from `gather_context` (#2358) re-assigned from `{run_dir}` above. Every lane receives `gsd-review-prompt.md` whole — it is never trimmed per lane (#4559). A lane whose prompt exceeds its cap is refused in `invoke_reviewers`, not shortened.
|
|
339
298
|
</step>
|
|
340
299
|
|
|
341
300
|
<step name="invoke_reviewers">
|
|
@@ -408,35 +367,6 @@ REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
|
|
|
408
367
|
# user's intent; here it would fire exactly the requests the default exists to prevent.
|
|
409
368
|
PARALLEL_LANES=$(gsd_run query config-get review.parallel_lanes --raw 2>/dev/null || echo "false")
|
|
410
369
|
|
|
411
|
-
# Shared budget-trim helper. Was defined inside the Ollama leg; it is lane-agnostic, so it is
|
|
412
|
-
# hoisted here now that any lane may declare a promptBudgetKey. Returns non-zero when the budget
|
|
413
|
-
# is too small for the minimum review set (prompt-budget exit 2 / 11).
|
|
414
|
-
prepare_trimmed_prompt_for_reviewer() {
|
|
415
|
-
REVIEWER_KEY="$1"; REVIEWER_BUDGET="$2"; OUTPUT_PROMPT="$3"; OUTPUT_META="$4"
|
|
416
|
-
|
|
417
|
-
PLAN_FILE_ARGS=""
|
|
418
|
-
for p in "$RUN_DIR"/gsd-review-plan-*.md; do
|
|
419
|
-
[ -f "$p" ] && PLAN_FILE_ARGS="$PLAN_FILE_ARGS --plan-file $p"
|
|
420
|
-
done
|
|
421
|
-
PROJECT_ARG=""
|
|
422
|
-
[ -f "$RUN_DIR/gsd-review-project.md" ] && PROJECT_ARG="--project-file $RUN_DIR/gsd-review-project.md"
|
|
423
|
-
CONTEXT_ARG=""
|
|
424
|
-
[ -f "$RUN_DIR/gsd-review-context.md" ] && CONTEXT_ARG="--context-file $RUN_DIR/gsd-review-context.md"
|
|
425
|
-
RESEARCH_ARG=""
|
|
426
|
-
[ -f "$RUN_DIR/gsd-review-research.md" ] && RESEARCH_ARG="--research-file $RUN_DIR/gsd-review-research.md"
|
|
427
|
-
REQUIREMENTS_ARG=""
|
|
428
|
-
[ -f "$RUN_DIR/gsd-review-requirements.md" ] && REQUIREMENTS_ARG="--requirements-file $RUN_DIR/gsd-review-requirements.md"
|
|
429
|
-
|
|
430
|
-
gsd_run query prompt-budget \
|
|
431
|
-
--budget "$REVIEWER_BUDGET" \
|
|
432
|
-
--instructions-file "$RUN_DIR/gsd-review-instructions.md" \
|
|
433
|
-
--roadmap-file "$RUN_DIR/gsd-review-roadmap.md" \
|
|
434
|
-
$PLAN_FILE_ARGS $PROJECT_ARG $CONTEXT_ARG $RESEARCH_ARG $REQUIREMENTS_ARG \
|
|
435
|
-
--output-prompt "$OUTPUT_PROMPT" \
|
|
436
|
-
--output-metadata "$OUTPUT_META"
|
|
437
|
-
return $?
|
|
438
|
-
}
|
|
439
|
-
|
|
440
370
|
gsd_run query review-lane plan \
|
|
441
371
|
--selected "$SELECTED_REVIEWERS" --run-dir "$RUN_DIR" --repo-root "$REPO_ROOT" --json \
|
|
442
372
|
> "$RUN_DIR/gsd-review-lanes.json"
|
|
@@ -453,36 +383,18 @@ run_review_lane() {
|
|
|
453
383
|
# `local` is hygiene, not a live fix: each `&`-dispatched call already forks its own subshell, so
|
|
454
384
|
# concurrent lanes cannot share these today. Scoped anyway so the isolation is a property of this
|
|
455
385
|
# function rather than of the dispatch mechanism happening to fork.
|
|
456
|
-
local SLUG
|
|
386
|
+
local SLUG
|
|
457
387
|
SLUG="$1"
|
|
458
|
-
#
|
|
459
|
-
#
|
|
460
|
-
#
|
|
461
|
-
|
|
462
|
-
--repo-root "$REPO_ROOT" --json 2>/dev/null \
|
|
463
|
-
| sed -n 's/.*"promptBudget": *\([0-9-]*\).*/\1/p' | head -1)
|
|
464
|
-
PROMPT_ARG=""
|
|
465
|
-
if [ -n "$LANE_BUDGET" ] && [ "$LANE_BUDGET" != "null" ] && [ "$LANE_BUDGET" -gt 0 ] 2>/dev/null; then
|
|
466
|
-
TRIMMED="$RUN_DIR/gsd-review-prompt-$SLUG.md"
|
|
467
|
-
if prepare_trimmed_prompt_for_reviewer "$SLUG" "$LANE_BUDGET" "$TRIMMED" \
|
|
468
|
-
"$RUN_DIR/gsd-review-prompt-$SLUG.metadata.json"; then
|
|
469
|
-
PROMPT_ARG="--prompt-file $TRIMMED"
|
|
470
|
-
else
|
|
471
|
-
# A budget too small for the minimum review set drops the lane just as silently as an empty
|
|
472
|
-
# response used to (#2605), so leave the skip visible in the review output, not only on stderr.
|
|
473
|
-
echo "$SLUG review skipped: prompt budget (${LANE_BUDGET} tokens) too small for the minimum review set." \
|
|
474
|
-
> "$RUN_DIR/gsd-review-$SLUG.md"
|
|
475
|
-
# Was `continue` when this was a loop body. Inside a function that keyword is not the loop
|
|
476
|
-
# control it looks like — `return 0` is what skips this lane and leaves it with no result line.
|
|
477
|
-
return 0
|
|
478
|
-
fi
|
|
479
|
-
fi
|
|
388
|
+
# #4559: no per-lane trimming. Every lane receives the whole prompt; `review-lane invoke` measures it
|
|
389
|
+
# against the lane's cap (its own `promptBudgetKey`, the global `review.max_prompt_tokens`, or the
|
|
390
|
+
# default derived from its declared context window) and REFUSES a lane that is over, writing a
|
|
391
|
+
# `review did not run: prompt exceeds` stub. A trimmed prompt reviewed something other than the plans.
|
|
480
392
|
|
|
481
393
|
# One invocation, whatever the lane's transport, prompt channel, output channel or handler.
|
|
482
394
|
# `--explicit` marks a lane the user NAMED: ADR-2782 D4 — not finding a lane nobody asked for is
|
|
483
395
|
# normal, failing to run one somebody asked for is an error.
|
|
484
396
|
gsd_run query review-lane invoke --slug "$SLUG" \
|
|
485
|
-
--run-dir "$RUN_DIR" --repo-root "$REPO_ROOT" $
|
|
397
|
+
--run-dir "$RUN_DIR" --repo-root "$REPO_ROOT" $EXPLICIT_FLAG --json \
|
|
486
398
|
> "$RUN_DIR/gsd-review-lane-result-$SLUG.json"
|
|
487
399
|
}
|
|
488
400
|
|
|
@@ -527,13 +439,17 @@ wait
|
|
|
527
439
|
|
|
528
440
|
# Aggregate in SELECTED_REVIEWERS order, NOT completion order, so the JSONL a concurrent run
|
|
529
441
|
# produces is byte-identical to the one a sequential run produces. This is post-join and therefore
|
|
530
|
-
# single-threaded, so `>>` here is safe. A lane that
|
|
531
|
-
#
|
|
442
|
+
# single-threaded, so `>>` here is safe. A lane that never started leaves no result file, and a lane
|
|
443
|
+
# REFUSED for its prompt cap (#4559: reason `prompt_exceeds_context`) is deliberately left out of the
|
|
444
|
+
# aggregate — it did not run, so it must not count as a lane result or feed models:/model_sources:.
|
|
445
|
+
# Its own result file stays in RUN_DIR for the `refused_reviewers` frontmatter.
|
|
532
446
|
# Rewrapped through unquoted command substitution (gsd-core#4109) — see the
|
|
533
447
|
# dispatch loop above for why a bare `$DISPATCH_SLUGS` collapses under zsh.
|
|
534
448
|
for SLUG in $(printf '%s' "$DISPATCH_SLUGS"); do
|
|
535
449
|
LANE_RESULT="$RUN_DIR/gsd-review-lane-result-$SLUG.json"
|
|
536
|
-
|
|
450
|
+
# The refusal is the PARSED `reason` field — a failed lane whose `detail` merely mentions the
|
|
451
|
+
# string is a real lane result and must stay in the aggregate.
|
|
452
|
+
if [ -f "$LANE_RESULT" ] && ! node -e 'try{process.exit(JSON.parse(require("fs").readFileSync(process.argv[1],"utf8")).reason==="prompt_exceeds_context"?0:1)}catch{process.exit(1)}' "$LANE_RESULT"; then
|
|
537
453
|
cat "$LANE_RESULT" >> "$RUN_DIR/gsd-review-lane-results.jsonl"
|
|
538
454
|
fi
|
|
539
455
|
done
|
|
@@ -546,7 +462,9 @@ review: it keeps its "failed or returned empty output" header (#2494/#2605/#2794
|
|
|
546
462
|
|
|
547
463
|
A lane that will not run reports a typed reason rather than an empty file — `missing_binary`,
|
|
548
464
|
`probe_failed`, `probe_timeout`, `missing_required_binary`, `host_unreachable`,
|
|
549
|
-
`egress_host_changed`, `unknown_handler`, `
|
|
465
|
+
`egress_host_changed`, `unknown_handler`, `prompt_exceeds_context` (the prompt is over
|
|
466
|
+
the lane's cap — refused, never trimmed; its review file carries a `review did not run: prompt exceeds` stub).
|
|
467
|
+
**`egress_host_changed` means the lane
|
|
550
468
|
was consented to send plans to one destination and `.planning/config.json` now names another; it is
|
|
551
469
|
blocked, not silently redirected** (ADR-2782 D5).
|
|
552
470
|
|
|
@@ -563,7 +481,7 @@ Display progress:
|
|
|
563
481
|
**#3352 (ADR-3473 §8.5): no artifact from failed inputs.** Before rendering anything, gate on
|
|
564
482
|
whether any lane actually produced a result — "every lane failed" is exactly "the aggregate
|
|
565
483
|
JSONL has zero lines" (§`invoke_reviewers`'s aggregation loop already builds this file as a
|
|
566
|
-
byproduct; a lane that never started or was
|
|
484
|
+
byproduct; a lane that never started or was refused for its prompt cap contributes no line either way).
|
|
567
485
|
|
|
568
486
|
```bash
|
|
569
487
|
RUN_DIR="{run_dir}"
|
|
@@ -575,7 +493,7 @@ TOTAL_LANE_FAILURE="false"
|
|
|
575
493
|
ALL_LANES_SKIPPED="false"
|
|
576
494
|
if [ "${LANE_LINES:-0}" -eq 0 ]; then
|
|
577
495
|
# Zero lines means every dispatched lane left no result JSON — either every one
|
|
578
|
-
# was
|
|
496
|
+
# was refused for its prompt cap (N5: a refusal is not a failure) or every one actually failed
|
|
579
497
|
# to run. Re-derive the dispatched-slug set the same way invoke_reviewers did
|
|
580
498
|
# (SELECTED_REVIEWERS is a shell block boundary — recompute, do not assume the
|
|
581
499
|
# earlier step's local DISPATCH_SLUGS variable survived into this block).
|
|
@@ -586,10 +504,10 @@ if [ "${LANE_LINES:-0}" -eq 0 ]; then
|
|
|
586
504
|
esac
|
|
587
505
|
DISPATCH_SLUGS="$DISPATCH_SLUGS $SLUG"
|
|
588
506
|
done
|
|
589
|
-
# Distinguish by whether every dispatched slug's stub markdown
|
|
590
|
-
#
|
|
591
|
-
#
|
|
592
|
-
#
|
|
507
|
+
# Distinguish by whether every dispatched slug's stub markdown carries the refusal marker:
|
|
508
|
+
# `review-lane invoke` writes `review did not run: prompt exceeds ...` (#4559) before returning
|
|
509
|
+
# without ever probing or spawning the lane; a real failure stub does not. If a slug has no
|
|
510
|
+
# stub at all, it is not a refusal.
|
|
593
511
|
DISPATCHED_COUNT=0
|
|
594
512
|
SKIPPED_COUNT=0
|
|
595
513
|
# Rewrapped through unquoted command substitution (gsd-core#4109): a bare
|
|
@@ -598,7 +516,7 @@ if [ "${LANE_LINES:-0}" -eq 0 ]; then
|
|
|
598
516
|
for SLUG in $(printf '%s' "$DISPATCH_SLUGS"); do
|
|
599
517
|
DISPATCHED_COUNT=$((DISPATCHED_COUNT + 1))
|
|
600
518
|
STUB="$RUN_DIR/gsd-review-$SLUG.md"
|
|
601
|
-
if [ -f "$STUB" ] && grep -q "review
|
|
519
|
+
if [ -f "$STUB" ] && grep -q "review did not run: prompt exceeds" "$STUB" 2>/dev/null; then
|
|
602
520
|
SKIPPED_COUNT=$((SKIPPED_COUNT + 1))
|
|
603
521
|
fi
|
|
604
522
|
done
|
|
@@ -611,15 +529,16 @@ fi
|
|
|
611
529
|
```
|
|
612
530
|
|
|
613
531
|
- **If `ALL_LANES_SKIPPED=true`:** do NOT write `REVIEWS.md` and do NOT run the commit below —
|
|
614
|
-
there is nothing to review. Report to the user that every selected lane
|
|
615
|
-
(not a failure
|
|
532
|
+
there is nothing to review. Report to the user that every selected lane refused its prompt cap
|
|
533
|
+
(not a failure; name the cap and the `review.context_window_tokens.<slug>` /
|
|
534
|
+
`review.max_prompt_tokens_per_reviewer.<slug>` keys that raise it) and stop; do not proceed to `present_results`' summary claiming a review ran.
|
|
616
535
|
- **If `TOTAL_LANE_FAILURE=true`:** do NOT write `REVIEWS.md` and do NOT run the commit below.
|
|
617
536
|
Report the total lane failure to the user (name the lanes that were dispatched and point at
|
|
618
537
|
their `.err`/stub files preserved under `.review-diagnostics/` by `present_results`) and stop.
|
|
619
538
|
- **Otherwise** (at least one lane produced a result — R1, unchanged): proceed exactly as below.
|
|
620
539
|
|
|
621
540
|
**#3301: plan coverage check.** For each dispatched lane that produced a *real* review (not a
|
|
622
|
-
stub, not
|
|
541
|
+
stub, not refused for its prompt cap, not empty), check whether its output mentions every plan id from
|
|
623
542
|
`.plans-manifest.md` — the same manifest `build_prompt` gave the reviewer, so the expected-id list
|
|
624
543
|
here can never diverge from what the reviewer was actually told. This is diagnostic only: it never
|
|
625
544
|
blocks the workflow, never fails a lane, and never changes the `TOTAL_LANE_FAILURE`/
|
|
@@ -673,7 +592,7 @@ for SLUG in $(printf '%s' "$DISPATCH_SLUGS"); do
|
|
|
673
592
|
REVIEW_FILE="$RUN_DIR/gsd-review-$SLUG.md"
|
|
674
593
|
[ -f "$REVIEW_FILE" ] || continue
|
|
675
594
|
[ -s "$REVIEW_FILE" ] || continue
|
|
676
|
-
grep -q "review
|
|
595
|
+
grep -q "review did not run: prompt exceeds" "$REVIEW_FILE" 2>/dev/null && continue
|
|
677
596
|
grep -q "failed or returned empty output" "$REVIEW_FILE" 2>/dev/null && continue
|
|
678
597
|
|
|
679
598
|
node -e '
|
|
@@ -696,7 +615,7 @@ done
|
|
|
696
615
|
|
|
697
616
|
Each `${RUN_DIR}/.plan-coverage-<slug>.json` carries `{complete, missing_ids, total}` for one
|
|
698
617
|
graded lane. Collect these into a `plan_coverage` frontmatter block — **only** when at least one
|
|
699
|
-
graded lane has `complete: false` (mirrors the
|
|
618
|
+
graded lane has `complete: false` (mirrors the `refused_reviewers` precedent: present
|
|
700
619
|
only when there is something to report):
|
|
701
620
|
|
|
702
621
|
```yaml
|
|
@@ -714,7 +633,7 @@ heading and before the end of the first exact `<!-- gsd:plan-revision-conflicts:
|
|
|
714
633
|
as `{preserved_plan_revision_conflict_entries}`. Ignore identical headings or delimiters in reviewer
|
|
715
634
|
output: reviewers do not own blocking state. Restore the captured bytes at the explicit slot below.
|
|
716
635
|
|
|
717
|
-
After all reviewers complete, collect
|
|
636
|
+
After all reviewers complete, collect the lanes that were refused for their prompt cap (#4559): each dispatched lane whose `${RUN_DIR}/gsd-review-lane-result-<slug>.json` has `"reason": "prompt_exceeds_context"`. For each, include a `refused_reviewers` entry in the frontmatter with `estimated_tokens`, `cap` and `cap_source` copied from that file's `refusal` object (`estimatedTokens`, `cap`, `capSource`). Omit the key entirely if no lane was refused. A refused lane's section in the body renders its `review did not run: prompt exceeds` stub, never an empty section.
|
|
718
637
|
|
|
719
638
|
**Reviewer instances (#1517, optional):** when instances ran, frontmatter records their
|
|
720
639
|
names, each gets its own `## <Adapter> Review (<instance>)` section, and ≥2 same-cli
|
|
@@ -746,16 +665,11 @@ models: # resolved model per reviewer; `unknown` when not reco
|
|
|
746
665
|
model_sources: # how each value above was determined
|
|
747
666
|
codex: "banner"
|
|
748
667
|
antigravity: "unknown"
|
|
749
|
-
|
|
668
|
+
refused_reviewers: # only present if at least one lane was refused for its prompt cap (#4559)
|
|
750
669
|
ollama:
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
omitted: [context, research]
|
|
755
|
-
project_md_shrunk: true
|
|
756
|
-
plan_truncation_pct: 22
|
|
757
|
-
hard_failed: false
|
|
758
|
-
note_injected: true
|
|
670
|
+
estimated_tokens: 61000
|
|
671
|
+
cap: 6000
|
|
672
|
+
cap_source: lane # lane | global | context-window
|
|
759
673
|
plan_coverage: # only present if at least one graded lane is incomplete (#3301)
|
|
760
674
|
ollama:
|
|
761
675
|
total: 7
|
|
@@ -819,7 +733,7 @@ present as complete. Report instead:
|
|
|
819
733
|
```
|
|
820
734
|
### GSD ► REVIEW FAILED
|
|
821
735
|
|
|
822
|
-
Phase {N}: every selected reviewer lane {failed to produce a result|
|
|
736
|
+
Phase {N}: every selected reviewer lane {failed to produce a result|refused its prompt cap} — no
|
|
823
737
|
REVIEWS.md was written.
|
|
824
738
|
|
|
825
739
|
{If the preserve+cleanup block below reports success: "Diagnostics preserved:
|
|
@@ -852,11 +766,9 @@ step names only `{padded_phase}-REVIEWS.md` explicitly, never a directory glob,
|
|
|
852
766
|
`.review-diagnostics/` is never swept into it.
|
|
853
767
|
|
|
854
768
|
**#4097: preserve lane OUTPUT, never the run's own input copies.** `RUN_DIR` holds not only
|
|
855
|
-
lane outputs — prompt assembly
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
requirements sections, and the per-lane trimmed prompts. Those are byte-identical duplicates of
|
|
859
|
-
files already committed under `.planning/`; sweeping them into `.review-diagnostics/` buries
|
|
769
|
+
lane outputs — prompt assembly also writes the run's assembled INPUT there under the same
|
|
770
|
+
`gsd-review-` prefix: the combined prompt (`gsd-review-prompt.md`). It duplicates files already
|
|
771
|
+
committed under `.planning/`; sweeping them into `.review-diagnostics/` buries
|
|
860
772
|
the actual evidence under plan duplicates and grows the phase directory on every run. The
|
|
861
773
|
exclusion list below is CLOSED and owned here: this workflow itself writes every input
|
|
862
774
|
basename at prompt-assembly time, so a future input file CANNOT silently join the evidence
|
|
@@ -882,8 +794,10 @@ DIAG_DIR="{phase_dir}/.review-diagnostics"
|
|
|
882
794
|
# the closed input set this workflow itself writes — direct glob iteration with a `case`
|
|
883
795
|
# filter, no string accumulator, identical under bash and zsh (#4099/#4109), and the
|
|
884
796
|
# `nullglob` set at the top of this fence keeps an empty RUN_DIR an empty array (#2962).
|
|
885
|
-
# `gsd-review-prompt*`
|
|
886
|
-
#
|
|
797
|
+
# `gsd-review-prompt*` is the one input file this workflow now writes (#4559 removed the
|
|
798
|
+
# per-lane trimmed prompts and the section copies). The section-copy names stay excluded:
|
|
799
|
+
# a run dir reused from an earlier GSD version can still hold them, and they are inputs,
|
|
800
|
+
# not lane evidence.
|
|
887
801
|
_DIAG_MD=()
|
|
888
802
|
for f in "$RUN_DIR"/gsd-review-*.md; do
|
|
889
803
|
case "$(basename "$f")" in
|
|
@@ -908,9 +822,8 @@ if [ ${#_DIAG_MD[@]} -gt 0 ] || [ ${#_DIAG_ERR[@]} -gt 0 ]; then
|
|
|
908
822
|
# unique per run BY CONSTRUCTION — uniqueness never depends on the clock. The
|
|
909
823
|
# UTC stamp is only a sort key in front of it, and is omitted entirely if `date`
|
|
910
824
|
# fails. Lane basenames are unchanged INSIDE the subdirectory, so evidence still
|
|
911
|
-
# correlates back to its lane, and nothing inside $RUN_DIR is renamed (
|
|
912
|
-
#
|
|
913
|
-
# on those exact basenames).
|
|
825
|
+
# correlates back to its lane, and nothing inside $RUN_DIR is renamed (the
|
|
826
|
+
# lane invocation resolver depends on those exact basenames).
|
|
914
827
|
_DIAG_STAMP="$(date -u +%Y%m%dT%H%M%SZ 2>/dev/null || true)"
|
|
915
828
|
_DIAG_RUN_DIR="$DIAG_DIR/${_DIAG_STAMP:+${_DIAG_STAMP}-}$(basename "$RUN_DIR")"
|
|
916
829
|
# `mkdir -p` still creates $DIAG_DIR itself, so a plain FILE sitting at
|
|
@@ -181,7 +181,7 @@ gsd_run query commit "docs(phase-${PHASE}): add/update security threat verificat
|
|
|
181
181
|
GSD > PHASE {N} THREAT-SECURE
|
|
182
182
|
threats_open: 0 — no blocking threats remain (threats_open: 0).
|
|
183
183
|
▶ /gsd:validate-phase {N} validate test coverage
|
|
184
|
-
▶ /gsd:verify-work {N} run UAT
|
|
184
|
+
▶ /gsd:verify-work {N} run UAT; once UAT and verification pass it marks the phase complete (ROADMAP + STATE); if it refuses on stale verification, re-run /gsd:execute-phase {N} then verify again
|
|
185
185
|
```
|
|
186
186
|
|
|
187
187
|
Display `/clear` reminder.
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
Apply response_language to all user-facing prose — narration between tool calls, status updates, progress notes, and findings included; preserve code, paths, and identifiers.
|
|
2
2
|
|
|
3
3
|
<purpose>
|
|
4
|
-
Interactive configuration of GSD power-user knobs — plan bounce, planner stall detection, node
|
|
5
|
-
repair, subagent timeouts, inline plan threshold, cross-AI execution, base branch, branch templates, response language,
|
|
4
|
+
Interactive configuration of GSD power-user knobs — plan bounce, planner stall detection and timing, node repair, subagent timeouts, inline plan threshold, cross-AI execution, base branch, branch templates, response language,
|
|
6
5
|
context window, gitignored search, graphify build timeout, runtime model tier overrides, and
|
|
7
6
|
model policy configuration (provider + budget → canonical tier mapping, or manual model ID
|
|
8
7
|
assignment per cost tier).
|
|
@@ -50,6 +49,8 @@ shown in parentheses:
|
|
|
50
49
|
|
|
51
50
|
Planning Tuning:
|
|
52
51
|
- `planner.stall_detection_enabled` (default: `true`)
|
|
52
|
+
- `planner.stall_threshold_minutes` (default: `10`)
|
|
53
|
+
- `planner.stall_detect_interval_minutes` (default: `5`)
|
|
53
54
|
- `workflow.plan_bounce` (default: `false`)
|
|
54
55
|
- `workflow.plan_bounce_passes` (default: `2`)
|
|
55
56
|
- `workflow.plan_bounce_script` (default: `null`)
|
|
@@ -106,10 +107,10 @@ in `$ARGUMENTS` OR `text_mode` is true in config. When `TEXT_MODE=true`, replace
|
|
|
106
107
|
choice number or free-text value.
|
|
107
108
|
|
|
108
109
|
**Numeric-input validation.** For any numeric field (`*_passes`, `*_budget`, `*_timeout`,
|
|
109
|
-
`*_threshold`, `context_window`, `graphify.build_timeout`), if the user types a value that
|
|
110
|
+
`*_threshold`, `*_minutes`, `context_window`, `graphify.build_timeout`), if the user types a value that
|
|
110
111
|
is not a non-negative integer, the workflow MUST reject it, state which value was invalid,
|
|
111
112
|
and re-prompt that single field. The minimum accepted value is field-specific and is stated
|
|
112
|
-
in each field's prompt below — `
|
|
113
|
+
in each field's prompt below — the `*_passes` fields and the `planner.stall_*_minutes` fields
|
|
113
114
|
require `>= 1`; all other numeric fields accept `>= 0`. An empty input means "keep current"
|
|
114
115
|
— the existing value is retained. Non-numeric input is never silently coerced.
|
|
115
116
|
|
|
@@ -127,7 +128,9 @@ stored verbatim as a string.
|
|
|
127
128
|
|
|
128
129
|
### Section 1 — Planning Tuning
|
|
129
130
|
|
|
130
|
-
|
|
131
|
+
Asked as four calls, 1a–1d (`AskUserQuestion` takes at most 4 questions per call).
|
|
132
|
+
|
|
133
|
+
**1a — Planner watchdog.** Before offering the disabled choice, display this warning verbatim:
|
|
131
134
|
|
|
132
135
|
`Warning: disabling planner stall detection gives up bounded automatic recovery if the runtime loses an agent completion handoff. Planning still waits through the runtime-native completion mechanism, but you may need to interrupt and use the filesystem fallback.`
|
|
133
136
|
|
|
@@ -141,7 +144,40 @@ AskUserQuestion([
|
|
|
141
144
|
{ label: "Yes (default: true)", description: "Poll for completion markers and plan-file activity, with bounded recovery." },
|
|
142
145
|
{ label: "No (false)", description: "Await runtime-native completion without polling. Gives up bounded automatic recovery if completion handoff is lost." }
|
|
143
146
|
]
|
|
147
|
+
}
|
|
148
|
+
])
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
**1b — Stall timing.** Ask only when `planner.stall_detection_enabled` is effectively `true` after 1a (anything but boolean `false`); otherwise skip 1b.
|
|
152
|
+
`planner.stall_threshold_minutes`: idle time before plan-phase offers recovery; raise it for slow models. `planner.stall_detect_interval_minutes`: check cadence. Both integer `>= 1`.
|
|
153
|
+
|
|
154
|
+
```text
|
|
155
|
+
AskUserQuestion([
|
|
156
|
+
{
|
|
157
|
+
question: "Planner stall threshold (minutes)? (current: <value or 10>)",
|
|
158
|
+
header: "Stall Threshold",
|
|
159
|
+
multiSelect: false,
|
|
160
|
+
options: [
|
|
161
|
+
{ label: "Keep current", description: "Leave the threshold unchanged." },
|
|
162
|
+
{ label: "Enter minutes", description: "Integer >= 1. Default: 10." }
|
|
163
|
+
]
|
|
144
164
|
},
|
|
165
|
+
{
|
|
166
|
+
question: "Planner stall check interval (minutes)? (current: <value or 5>)",
|
|
167
|
+
header: "Stall Interval",
|
|
168
|
+
multiSelect: false,
|
|
169
|
+
options: [
|
|
170
|
+
{ label: "Keep current", description: "Leave the interval unchanged." },
|
|
171
|
+
{ label: "Enter minutes", description: "Integer >= 1. Default: 5." }
|
|
172
|
+
]
|
|
173
|
+
}
|
|
174
|
+
])
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
**1c — Plan bounce.**
|
|
178
|
+
|
|
179
|
+
```text
|
|
180
|
+
AskUserQuestion([
|
|
145
181
|
{
|
|
146
182
|
question: "Run external plan-bounce validator against generated PLAN.md? (current: <value or false>)",
|
|
147
183
|
header: "Plan Bounce",
|
|
@@ -169,7 +205,14 @@ AskUserQuestion([
|
|
|
169
205
|
{ label: "Clear (null)", description: "Unset the script path." },
|
|
170
206
|
{ label: "Enter path", description: "Type an absolute or repo-relative path. Receives PLAN.md path as first argument." }
|
|
171
207
|
]
|
|
172
|
-
}
|
|
208
|
+
}
|
|
209
|
+
])
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
**1d — Timeouts.**
|
|
213
|
+
|
|
214
|
+
```text
|
|
215
|
+
AskUserQuestion([
|
|
173
216
|
{
|
|
174
217
|
question: "Subagent timeout (milliseconds)? (current: <value or 300000>)",
|
|
175
218
|
header: "Subagent Timeout",
|
|
@@ -180,12 +223,12 @@ AskUserQuestion([
|
|
|
180
223
|
]
|
|
181
224
|
},
|
|
182
225
|
{
|
|
183
|
-
question: "Inline plan threshold — tasks
|
|
226
|
+
question: "Inline plan threshold — plans with this many tasks or fewer execute inline instead of spawning a subagent? (current: <value or 2>)",
|
|
184
227
|
header: "Inline Plan Threshold",
|
|
185
228
|
multiSelect: false,
|
|
186
229
|
options: [
|
|
187
230
|
{ label: "Keep current", description: "Leave threshold unchanged." },
|
|
188
|
-
{ label: "Enter number", description: "Integer
|
|
231
|
+
{ label: "Enter number", description: "Integer >= 0. Non-numeric rejected. Default: 2. Set 0 to always spawn a subagent." }
|
|
189
232
|
]
|
|
190
233
|
}
|
|
191
234
|
])
|
|
@@ -535,7 +578,9 @@ anything not listed in Sections 1–8 MUST survive the update):
|
|
|
535
578
|
...existing_config,
|
|
536
579
|
"planner": {
|
|
537
580
|
...existing_planner,
|
|
538
|
-
"stall_detection_enabled": <new|existing
|
|
581
|
+
"stall_detection_enabled": <new|existing>,
|
|
582
|
+
"stall_threshold_minutes": <new|existing>,
|
|
583
|
+
"stall_detect_interval_minutes": <new|existing>
|
|
539
584
|
},
|
|
540
585
|
"workflow": {
|
|
541
586
|
...existing_workflow,
|
|
@@ -772,6 +817,8 @@ Display:
|
|
|
772
817
|
| Setting | Value |
|
|
773
818
|
|--------------------------------------------|-------|
|
|
774
819
|
| planner.stall_detection_enabled | {true/false} |
|
|
820
|
+
| planner.stall_threshold_minutes | {minutes} |
|
|
821
|
+
| planner.stall_detect_interval_minutes | {minutes} |
|
|
775
822
|
| workflow.plan_bounce | {on/off} |
|
|
776
823
|
| workflow.plan_bounce_passes | {n} |
|
|
777
824
|
| workflow.plan_bounce_script | {path/null} |
|
|
@@ -826,6 +873,7 @@ UI/AI phase gates), use /gsd:settings.
|
|
|
826
873
|
- [ ] Eight sections rendered (Planning, Execution, Discussion, Cross-AI, Git, Runtime/Output, Runtime Model Tiers, Model Policy)
|
|
827
874
|
- [ ] Every field pre-selected to its current value (or documented default if absent)
|
|
828
875
|
- [ ] Disabling planner stall detection shows the bounded-recovery warning before writing `false`
|
|
876
|
+
- [ ] Stall threshold/interval asked only while stall detection is on; no call exceeds 4 questions
|
|
829
877
|
- [ ] Numeric inputs validated — non-numeric rejected and re-prompted
|
|
830
878
|
- [ ] Branch-template inputs validated — non-default must contain a placeholder
|
|
831
879
|
- [ ] Null-allowed fields accept an empty input as a clear
|
|
@@ -73,6 +73,18 @@ Verify the work is ready to ship:
|
|
|
73
73
|
```
|
|
74
74
|
Present `$NEXT_ACTION` to the user and, when `$NEXT_COMMAND` is non-empty, show it as the command to run next. These two are message text only — the block/allow decision has already been made from `$STATUS`, so a concurrent write between the reads cannot change the gate's verdict. The query already covers every status in its closed set, so no per-status arm is needed.
|
|
75
75
|
|
|
76
|
+
1a. **Phase marked complete?** (#5221) Verified is not complete: `/gsd:verify-work` completes the phase.
|
|
77
|
+
```bash
|
|
78
|
+
ANALYZE=$(gsd_run query roadmap.analyze ${GSD_WS:+--ws=${GSD_WS##* }}) || { echo "roadmap.analyze failed — see the error above" >&2; exit 1; }
|
|
79
|
+
if [[ "$ANALYZE" == @file:* ]]; then ANALYZE=$(cat "${ANALYZE#@file:}"); fi
|
|
80
|
+
# roadmap_record: phase -> every ROADMAP record of it reads complete. Fail closed: only true ships.
|
|
81
|
+
VERDICT=$(jq -er --arg p "$PHASE_NUMBER" 'def n:split(".")|map(sub("^0+(?=[0-9])";""))|join(".");if .error=="ROADMAP.md not found" then "skip" elif has("roadmap_record")|not then error("no roadmap_record") else .roadmap_record as $r|($p|n) as $k|if $r==null then "unreadable" elif ($r|type)!="object" then error("bad roadmap_record") elif ($r|has($k)|not) then "unrecorded" elif $r[$k] then "complete" else "incomplete" end end' <<<"$ANALYZE") || { echo "roadmap.analyze output unparseable" >&2; exit 1; }
|
|
82
|
+
[ "$VERDICT" != unreadable ] || { echo "ROADMAP_TABLE_UNREADABLE: the ROADMAP.md progress table does not parse (e.g. a row with the wrong cell count); fix it, then re-run /gsd:ship" >&2; exit 1; }
|
|
83
|
+
[ "$VERDICT" != unrecorded ] || { echo "PHASE_NOT_RECORDED: ROADMAP.md has no checklist entry or progress-table row for phase ${PHASE_NUMBER}; add one, run /gsd:verify-work ${PHASE_NUMBER}, then re-run /gsd:ship" >&2; exit 1; }
|
|
84
|
+
[ "$VERDICT" != incomplete ] || { echo "PHASE_NOT_COMPLETE: Run /gsd:verify-work ${PHASE_NUMBER} (completes the phase), then re-run /gsd:ship" >&2; [[ $PHASE_NUMBER =~ ^[A-Za-z]?[0-9] ]] || echo "Custom-id phase: the phase tools cannot find its directory yet, so after verifying, tick its ROADMAP.md checklist entry by hand" >&2; exit 1; }
|
|
85
|
+
```
|
|
86
|
+
Only a phase whose every ROADMAP record (checklist entry, progress-table Status) reads complete ships; a phase with no record blocks. A missing ROADMAP.md skips the check; an analyze failure stops. This reads the record to catch a missed write-back — it is not a completion verdict (ADR-3180 §7.4, Amendment 10; step 1 already holds the disk verdict). `roadmap_record` keys use `foldPhaseId`; `def n` above must fold the same way.
|
|
87
|
+
|
|
76
88
|
2. **Clean working tree?**
|
|
77
89
|
```bash
|
|
78
90
|
git status --short
|
|
@@ -81,6 +81,12 @@ case "$COMMAND" in
|
|
|
81
81
|
*) exit 0 ;;
|
|
82
82
|
esac
|
|
83
83
|
|
|
84
|
+
# #5548: inherited repo-location vars (GIT_DIR, GIT_INDEX_FILE, ...) would override
|
|
85
|
+
# cwd for every git call below. Drop git's own list minus the config-injection vars.
|
|
86
|
+
for v in $(git rev-parse --local-env-vars 2>/dev/null); do
|
|
87
|
+
case "$v" in GIT_CONFIG|GIT_CONFIG_PARAMETERS|GIT_CONFIG_COUNT) ;; *) unset "$v" ;; esac
|
|
88
|
+
done
|
|
89
|
+
|
|
84
90
|
# Gate 3 — inside git repo
|
|
85
91
|
git rev-parse --git-dir >/dev/null 2>&1 || exit 0
|
|
86
92
|
|
|
@@ -18,6 +18,7 @@ const os = require('os');
|
|
|
18
18
|
// monkeypatching childProcess.execFileSync.
|
|
19
19
|
const childProcess = require('child_process');
|
|
20
20
|
const { HOOK_ON_CRASH, allow, crash } = require('./lib/hook-exit.js');
|
|
21
|
+
const { gitEnv } = require('./lib/git-probe.js');
|
|
21
22
|
|
|
22
23
|
// This hook's build-seam outer catch (require.main guard just below) has
|
|
23
24
|
// always exited 0 (fail open — the statusline renders on EVERY prompt, so a
|
|
@@ -934,7 +935,7 @@ function readGitStatus(dir) {
|
|
|
934
935
|
// or untracked files; overflow still degrades safely to segment-absent via
|
|
935
936
|
// the catch below.
|
|
936
937
|
return childProcess.execFileSync('git', ['-C', dir, 'status', '--porcelain=v2', '--branch'],
|
|
937
|
-
{ encoding: 'utf8', timeout: GIT_STATUS_TIMEOUT_MS, maxBuffer: 8 * 1024 * 1024, stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true, env: readOnlyGitEnv() });
|
|
938
|
+
{ encoding: 'utf8', timeout: GIT_STATUS_TIMEOUT_MS, maxBuffer: 8 * 1024 * 1024, stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true, env: readOnlyGitEnv(gitEnv()) });
|
|
938
939
|
} catch (e) {
|
|
939
940
|
return null;
|
|
940
941
|
}
|
|
@@ -1029,7 +1030,7 @@ function readStateHeadCommits(root, stamp) {
|
|
|
1029
1030
|
try {
|
|
1030
1031
|
return childProcess.execFileSync('git', // allow-index-refresh: rev-list walks the commit graph and never opens the index
|
|
1031
1032
|
['-C', root, 'rev-list', '--left-right', '--count', `${stamp}...HEAD`],
|
|
1032
|
-
{ encoding: 'utf8', timeout: STATE_FRESHNESS_GIT_TIMEOUT_MS,
|
|
1033
|
+
{ env: gitEnv(), encoding: 'utf8', timeout: STATE_FRESHNESS_GIT_TIMEOUT_MS,
|
|
1033
1034
|
stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true });
|
|
1034
1035
|
} catch (e) {
|
|
1035
1036
|
return null;
|
|
@@ -31,7 +31,7 @@ const fs = require('fs');
|
|
|
31
31
|
const path = require('path');
|
|
32
32
|
const { spawnSync } = require('child_process');
|
|
33
33
|
const { allow, deny } = require('./lib/hook-exit.js');
|
|
34
|
-
const { reportIfUndetermined, BLOCKING_GUARD_PROBE_TIMEOUT_MS } = require('./lib/git-probe.js');
|
|
34
|
+
const { reportIfUndetermined, gitEnv, BLOCKING_GUARD_PROBE_TIMEOUT_MS } = require('./lib/git-probe.js');
|
|
35
35
|
|
|
36
36
|
// #3911 (ADR-3889 Phase 7): the exit(2) call site (block(), below) is
|
|
37
37
|
// migrated to hook-exit.js's deny(undefined, reason) — see
|
|
@@ -64,10 +64,10 @@ const SPAWNOPT = { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeou
|
|
|
64
64
|
// Parity: tests/git-optional-locks-parity.test.cjs.
|
|
65
65
|
// A raw plugin-marketplace or git-clone install never ran `npm run build:lib`
|
|
66
66
|
// (#3582), so the compiled owner can be absent. This guard only runs
|
|
67
|
-
// `git rev-parse`, which never opens the index, so the
|
|
67
|
+
// `git rev-parse`, which never opens the index, so the scrubbed gitEnv() base
|
|
68
68
|
// loses nothing; requiring the artifact unguarded would throw at load and the
|
|
69
69
|
// containment guard would silently stop running.
|
|
70
|
-
let readOnlyGitEnv = (
|
|
70
|
+
let readOnlyGitEnv = (base = process.env) => base;
|
|
71
71
|
try {
|
|
72
72
|
const { ensureRuntimeBuild } = require('../gsd-core/bin/ensure-runtime-build.cjs');
|
|
73
73
|
ensureRuntimeBuild();
|
|
@@ -77,7 +77,7 @@ try {
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
function git(args, cwd) {
|
|
80
|
-
return spawnSync('git', args, { ...SPAWNOPT, cwd, env: readOnlyGitEnv() });
|
|
80
|
+
return spawnSync('git', args, { ...SPAWNOPT, cwd, env: readOnlyGitEnv(gitEnv()) });
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
// Walk up from `start` to find the nearest existing DIRECTORY (not merely an
|