@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
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Necessity Review
|
|
2
|
+
|
|
3
|
+
Rubric for category 4 (Necessity) of `gsd-code-reviewer`. Read at `standard` and `deep` depth only, before classifying findings. It asks a question the other three categories do not: **does the problem this code addresses actually occur in this stack?** Correct, clean, well-tested code can still guard against nothing.
|
|
4
|
+
|
|
5
|
+
Taxonomy adapted from the `functionality-complexity-tradeoff` skill in [l-gevity/l-gevity-skills](https://github.com/l-gevity/l-gevity-skills) (MIT), #4100.
|
|
6
|
+
|
|
7
|
+
## Taxonomy
|
|
8
|
+
|
|
9
|
+
| Subtype | Signal | Example |
|
|
10
|
+
|---|---|---|
|
|
11
|
+
| **Impossible-state guard** | Defends against a state ruled out by the type system, the deployment topology, or a runtime invariant | Null-check on a value whose type is non-nullable end to end; client/server version-skew check in a single-artifact deploy |
|
|
12
|
+
| **Already-defended-elsewhere** | A concern fully owned by another layer, duplicated here | Manual HTML escaping on top of an auto-escaping template engine; manual rollback inside an outer transaction that already rolls back |
|
|
13
|
+
| **Cargo-culted pattern** | A pattern whose prerequisites do not hold in this context | Connection pool in a CLI that runs for 200 ms; singleton cache in a stateless lambda |
|
|
14
|
+
| **Phantom requirement** | Solves a requirement that was never real or has provably lapsed | Feature flag for a launch that completed and is on for every environment; a migration shim for data the repo proves was migrated everywhere |
|
|
15
|
+
| **Generality without instantiation** | An abstraction with exactly one instantiation for its whole lifetime | Strategy pattern with one strategy; plugin registry with one plugin; config key holding the same value in every environment |
|
|
16
|
+
|
|
17
|
+
## Evidence rule
|
|
18
|
+
|
|
19
|
+
Verify the premise before reviewing the implementation. A necessity finding must state **why the guarded state cannot occur** (or why the requirement lapsed) and cite where that is proven: the type signature, the build/deploy configuration, the call sites, the flag's values. The pattern name alone is not a finding — drop it.
|
|
20
|
+
|
|
21
|
+
## Severity
|
|
22
|
+
|
|
23
|
+
- **Warning** (`WR-`) — the impossibility or lapse is provable from the repository.
|
|
24
|
+
- **Info** (`IN-`) — it rests on judgment (likely, but not shown).
|
|
25
|
+
- **Never Critical.** Removal is a scope decision for a human, not a defect fix.
|
|
26
|
+
|
|
27
|
+
## Negative space — NOT necessity findings
|
|
28
|
+
|
|
29
|
+
- **Trust boundary.** A guard on data crossing a trust boundary (user input, network, files, environment, deserialized data, another process) is never an impossible-state guard, even when internal types claim the value is safe — types do not hold across a trust boundary.
|
|
30
|
+
- **Defense in depth.** A deliberate second security layer is not "already-defended-elsewhere".
|
|
31
|
+
- **Test seams.** A single-implementation interface that exists as a dependency-injection or test seam is not "generality without instantiation".
|
|
32
|
+
- **Unreachable code** is category 3 (Code Quality, dead code). Necessity is code that runs, but defends against nothing.
|
|
33
|
+
- **Unproven lapse.** A flag or migration whose lapse cannot be shown from the repository is Info at most.
|
|
34
|
+
|
|
35
|
+
## REVIEW.md format
|
|
36
|
+
|
|
37
|
+
A necessity finding keeps its normal ID and section (`WR-` under `## Warnings`, `IN-` under `## Info`), names its subtype in the title, and carries the category marker line. The marker is what keeps `gsd-code-fixer` from removing the code under the default fix scope:
|
|
38
|
+
|
|
39
|
+
```markdown
|
|
40
|
+
### WR-04: Impossible-state guard — client version check in single-artifact SPA
|
|
41
|
+
|
|
42
|
+
**File:** `src/api/client.ts:42`
|
|
43
|
+
**Category:** necessity
|
|
44
|
+
**Issue:** The guarded state (client/server version skew) cannot occur: both are built and deployed as one artifact (`vite.config.ts:12`, `deploy/Dockerfile:8`).
|
|
45
|
+
**Fix:** Remove the check; record the invariant in a comment or ADR rather than a runtime branch.
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Count every necessity finding in the frontmatter `necessity:` key. It is a subset of `warning` + `info`, never added to `total`.
|
|
@@ -270,7 +270,32 @@ if [ "$AUTO_MODE" = "true" ]; then
|
|
|
270
270
|
# It is re-derived there from the final REVIEW.md's status, which is exactly the condition the
|
|
271
271
|
# `break` below fires on. A variable set here and read nowhere would just be a decoy.
|
|
272
272
|
|
|
273
|
-
|
|
273
|
+
# #4100: the loop used to stop only on a re-review returning `status: clean`. A review whose
|
|
274
|
+
# remaining findings are all OUTSIDE the fix scope (Info, or necessity findings without --all)
|
|
275
|
+
# never reaches clean, so the loop re-spawned the fixer with nothing to fix until the cap and then
|
|
276
|
+
# printed the degradation warning over a run with no work left. The fixer is the scope authority:
|
|
277
|
+
# its REVIEW-FIX.md `findings_in_scope` is read strictly — only a frontmatter value of 0 proves
|
|
278
|
+
# zero scope; a missing report or an unusable value keeps the old behaviour.
|
|
279
|
+
_gsd_fix_scope_empty() {
|
|
280
|
+
[ -f "$1" ] || return 1
|
|
281
|
+
_gsd_n=$(FIX_REPORT_PATH="$1" node -e "
|
|
282
|
+
const fs = require('fs');
|
|
283
|
+
try {
|
|
284
|
+
const content = fs.readFileSync(process.env.FIX_REPORT_PATH, 'utf-8');
|
|
285
|
+
const m = content.replace(/\r\n/g, '\n').match(/^---\n([\s\S]*?)\n---/);
|
|
286
|
+
const v = m && m[1].match(/^findings_in_scope:[ \t]*([0-9]+)[ \t]*$/m);
|
|
287
|
+
process.stdout.write(v ? String(Number(v[1])) : '');
|
|
288
|
+
} catch (e) {}
|
|
289
|
+
" 2>/dev/null || true)
|
|
290
|
+
[ "$_gsd_n" = "0" ]
|
|
291
|
+
}
|
|
292
|
+
NO_SCOPE_LEFT=false
|
|
293
|
+
if _gsd_fix_scope_empty "${FIX_REPORT_PATH}"; then
|
|
294
|
+
NO_SCOPE_LEFT=true
|
|
295
|
+
echo "No findings in fix scope (${FIX_SCOPE}) — skipping the re-review loop. Out-of-scope findings remain in ${REVIEW_PATH}."
|
|
296
|
+
fi
|
|
297
|
+
|
|
298
|
+
while [ "$NO_SCOPE_LEFT" = false ] && [ $ITERATION -lt $MAX_ITERATIONS ]; do
|
|
274
299
|
ITERATION=$((ITERATION + 1))
|
|
275
300
|
|
|
276
301
|
echo ""
|
|
@@ -361,6 +386,12 @@ ${AGENT_SKILLS_FIXER}")
|
|
|
361
386
|
echo "Warning: Iteration ${ITERATION} fixer failed to produce fix report. Stopping auto-loop."
|
|
362
387
|
break
|
|
363
388
|
fi
|
|
389
|
+
if _gsd_fix_scope_empty "${FIX_REPORT_PATH}"; then
|
|
390
|
+
NO_SCOPE_LEFT=true
|
|
391
|
+
echo ""
|
|
392
|
+
echo "✓ No in-scope findings remain after iteration ${ITERATION} (scope: ${FIX_SCOPE}). Out-of-scope findings remain in ${REVIEW_PATH}."
|
|
393
|
+
break
|
|
394
|
+
fi
|
|
364
395
|
done
|
|
365
396
|
|
|
366
397
|
# After loop completes. The iteration COUNTER alone does not distinguish degradation from success:
|
|
@@ -369,7 +400,7 @@ ${AGENT_SKILLS_FIXER}")
|
|
|
369
400
|
# every finding was fixed — telling the operator the opposite of what happened, on the one path
|
|
370
401
|
# where the cap and convergence coincide. Convergence is re-derived from the review the loop left
|
|
371
402
|
# behind, the same signal cleanup_iteration_backups reads, so the two cannot disagree.
|
|
372
|
-
if [ $ITERATION -ge $MAX_ITERATIONS ]; then
|
|
403
|
+
if [ "$NO_SCOPE_LEFT" = false ] && [ $ITERATION -ge $MAX_ITERATIONS ]; then
|
|
373
404
|
LOOP_END_STATUS=$(REVIEW_PATH="${REVIEW_PATH}" node -e "
|
|
374
405
|
const fs = require('fs');
|
|
375
406
|
try {
|
|
@@ -491,8 +522,9 @@ which is the one distinction #3829 exists to make.
|
|
|
491
522
|
Convergence is re-derived here rather than carried. Shell state does not survive the loop's fence, so
|
|
492
523
|
a flag set there would be gone by this step — and once nothing in the loop consumed it, keeping it
|
|
493
524
|
would have left a variable set in one place and read in none. The loop breaks on exactly one
|
|
494
|
-
condition, a re-review returning clean, and that leaves `REVIEW.md` at
|
|
495
|
-
review's status IS the converged case.
|
|
525
|
+
condition that counts as convergence, a re-review returning clean, and that leaves `REVIEW.md` at
|
|
526
|
+
`status: clean`: the final review's status IS the converged case. Its other stop, nothing left in
|
|
527
|
+
fix scope (#4100), leaves out-of-scope findings in a non-clean review, so the backups are kept. Hitting `MAX_ITERATIONS` leaves the last re-review non-clean,
|
|
496
528
|
and the backups are kept.
|
|
497
529
|
|
|
498
530
|
```bash
|
|
@@ -586,7 +618,16 @@ echo ""
|
|
|
586
618
|
|
|
587
619
|
If status is "all_fixed":
|
|
588
620
|
```bash
|
|
589
|
-
if [ "$
|
|
621
|
+
if [ "${FINDINGS_IN_SCOPE:-}" = "0" ]; then
|
|
622
|
+
# #4100: a vacuous all_fixed (nothing was in scope) must not claim the review is resolved.
|
|
623
|
+
echo "○ No findings were in fix scope (${FIX_SCOPE}). Out-of-scope findings (Info, and necessity unless --all) remain in ${REVIEW_PATH}."
|
|
624
|
+
echo ""
|
|
625
|
+
if [ "${FIX_SCOPE}" != "all" ]; then
|
|
626
|
+
echo "Next step:"
|
|
627
|
+
echo " /gsd:code-review ${PHASE_NUMBER} --fix --all — Include them in the fix scope"
|
|
628
|
+
echo ""
|
|
629
|
+
fi
|
|
630
|
+
elif [ "$FIX_STATUS" = "all_fixed" ]; then
|
|
590
631
|
echo "✓ All issues resolved."
|
|
591
632
|
echo ""
|
|
592
633
|
echo "Full report: ${FIX_REPORT_PATH}"
|
|
@@ -932,6 +932,8 @@ CRITICAL=$(echo "$FRONTMATTER" | grep -E "^[[:space:]]*(critical|blocker):" | he
|
|
|
932
932
|
WARNING=$(echo "$FRONTMATTER" | grep "warning:" | head -1 | cut -d: -f2 | xargs)
|
|
933
933
|
INFO=$(echo "$FRONTMATTER" | grep "info:" | head -1 | cut -d: -f2 | xargs)
|
|
934
934
|
TOTAL=$(echo "$FRONTMATTER" | grep "total:" | head -1 | cut -d: -f2 | xargs)
|
|
935
|
+
# #4100: necessity is a SUBSET of warning+info, never part of total. Absent at quick depth.
|
|
936
|
+
NECESSITY=$(echo "$FRONTMATTER" | grep -E "^[[:space:]]*necessity:" | head -1 | cut -d: -f2 | sed 's/#.*//' | xargs)
|
|
935
937
|
```
|
|
936
938
|
|
|
937
939
|
Display inline summary to user:
|
|
@@ -950,6 +952,7 @@ Display inline summary to user:
|
|
|
950
952
|
Critical: ${CRITICAL}
|
|
951
953
|
Warning: ${WARNING}
|
|
952
954
|
Info: ${INFO}
|
|
955
|
+
Necessity: ${NECESSITY:-not assessed} (of the above; skipped by --fix unless --all)
|
|
953
956
|
|
|
954
957
|
---
|
|
955
958
|
Total: ${TOTAL}
|
|
@@ -958,7 +958,7 @@ increases monotonically across waves. `{status}` is `complete` (success),
|
|
|
958
958
|
if [ "${TEST_EXIT}" -eq 0 ]; then
|
|
959
959
|
# Update ROADMAP plan progress for each completed plan in this wave
|
|
960
960
|
for plan_id in {completed_plan_ids}; do
|
|
961
|
-
gsd_run query roadmap.update-plan-progress "${PHASE_NUMBER}" "${plan_id}" "complete"
|
|
961
|
+
PROGRESS=$(gsd_run query roadmap.update-plan-progress "${PHASE_NUMBER}" "${plan_id}" "complete") && REASON=$(jq -er '.reason//""' <<<"$PROGRESS") && [ "$REASON" != missing_phase_details ] || { echo "ERROR: ROADMAP update-plan-progress did not complete for ${PHASE_NUMBER}-${plan_id} (missing_phase_details decline, failed query, or jq not installed). STOP: skip this plan's completion steps." >&2; exit 1; }
|
|
962
962
|
done
|
|
963
963
|
|
|
964
964
|
# Only commit tracking files if they actually changed
|
|
@@ -504,7 +504,7 @@ IS_WORKTREE=$([ -f .git ] && echo "true" || echo "false")
|
|
|
504
504
|
|
|
505
505
|
if [ "$IS_WORKTREE" != "true" ]; then
|
|
506
506
|
# use_worktrees: false → this handler is the sole post-plan sync point (#2661)
|
|
507
|
-
gsd_run query roadmap.update-plan-progress "${PHASE}"
|
|
507
|
+
PROGRESS=$(gsd_run query roadmap.update-plan-progress "${PHASE}") && REASON=$(jq -er '.reason//""' <<<"$PROGRESS") && [ "$REASON" != missing_phase_details ] || { echo "ERROR: ROADMAP update-plan-progress did not complete for ${PHASE}-${PLAN} (missing_phase_details decline, failed query, or jq not installed). STOP: skip this plan's completion steps." >&2; exit 1; }
|
|
508
508
|
fi
|
|
509
509
|
```
|
|
510
510
|
Counts PLAN vs SUMMARY files on disk. Updates progress table row with correct count and status (`In Progress` or `Complete` with date).
|
|
@@ -103,23 +103,27 @@ PLANNER_STALL_THRESHOLD_MINUTES=$(gsd_run query config-get planner.stall_thresho
|
|
|
103
103
|
# a reliability risk this fix cannot afford: a malformed config value would
|
|
104
104
|
# abort the stall-watcher itself with a bash syntax error, silently defeating
|
|
105
105
|
# the exact hang-recovery this issue is about. Reject anything that is not a
|
|
106
|
-
# bare
|
|
107
|
-
# degrades to the safe default instead of crashing the watcher.
|
|
108
|
-
|
|
109
|
-
|
|
106
|
+
# bare positive integer before it is ever used, so a bad config value
|
|
107
|
+
# degrades to the safe default instead of crashing the watcher. Zero is
|
|
108
|
+
# rejected too (#3923): an interval of 0 makes every gsd_stall_watch cycle a
|
|
109
|
+
# zero-length sleep, and a threshold of 0 declares a healthy agent stalled on
|
|
110
|
+
# the first check. At most 9 digits, so `* 60` can never overflow.
|
|
111
|
+
[[ "$PLANNER_STALL_INTERVAL_MINUTES" =~ ^[1-9][0-9]{0,8}$ ]] || PLANNER_STALL_INTERVAL_MINUTES=5
|
|
112
|
+
[[ "$PLANNER_STALL_THRESHOLD_MINUTES" =~ ^[1-9][0-9]{0,8}$ ]] || PLANNER_STALL_THRESHOLD_MINUTES=10
|
|
110
113
|
|
|
111
114
|
# gsd_stall_should_recover — pure decision function, no IO, no sleeping. Given how
|
|
112
115
|
# long the orchestrator has been waiting plus two liveness signals (a completion
|
|
113
116
|
# marker found in the agent's output file, and fresh on-disk artifact activity),
|
|
114
117
|
# decides whether to keep waiting, treat the wait as satisfied, or auto-surface the
|
|
115
118
|
# existing accept/retry/stop recovery menu (9a/11a). Never kills or retries anything
|
|
116
|
-
# itself — it only classifies. Re-validates both numeric args as bare
|
|
117
|
-
# integers (defense in depth — safe to call with any input, not just
|
|
118
|
-
# config globals above) before either ever
|
|
119
|
+
# itself — it only classifies. Re-validates both numeric args as bounded bare
|
|
120
|
+
# non-negative integers (defense in depth — safe to call with any input, not just
|
|
121
|
+
# the resolved config globals above, which are always >= 1) before either ever
|
|
122
|
+
# reaches arithmetic expansion. It accepts 0 so callers can test boundaries.
|
|
119
123
|
gsd_stall_should_recover() {
|
|
120
124
|
local elapsed_seconds="$1" threshold_minutes="$2" marker_found="$3" artifact_fresh="$4"
|
|
121
|
-
[[ "$elapsed_seconds" =~ ^[0-9]
|
|
122
|
-
[[ "$threshold_minutes" =~ ^[0-9]
|
|
125
|
+
[[ "$elapsed_seconds" =~ ^[0-9]{1,15}$ ]] || elapsed_seconds=0
|
|
126
|
+
[[ "$threshold_minutes" =~ ^[0-9]{1,9}$ ]] || threshold_minutes=10
|
|
123
127
|
local threshold_seconds=$(( threshold_minutes * 60 ))
|
|
124
128
|
if [ "$marker_found" = "true" ]; then
|
|
125
129
|
echo "marker_received"; return 0
|
|
@@ -1441,7 +1441,7 @@ This step is derived entirely from existing PLAN frontmatter — no extra LLM pa
|
|
|
1441
1441
|
gsd_run query roadmap.annotate-dependencies "${PHASE_NUMBER}"
|
|
1442
1442
|
```
|
|
1443
1443
|
|
|
1444
|
-
|
|
1444
|
+
Skip this step if `plan_count` is 0. Re-running regenerates the annotation from current PLAN frontmatter; an unchanged result writes nothing, and an unrecognised checklist row leaves ROADMAP.md untouched and reports `skipped: "unrecognised_checklist_row"`. The `**Cross-cutting constraints:**` header line ends in an HTML-comment marker; that block is machine-owned and the bullets directly under it are regenerated. Put hand-written bullets after a blank line. Bullets kept from a block written before the marker existed are reported as `stale_legacy_constraints`.
|
|
1445
1445
|
|
|
1446
1446
|
## 13d. Commit Plans if commit_docs is true
|
|
1447
1447
|
|
|
@@ -90,6 +90,8 @@ jq dependency). Write dirty repo names to a temp file so the list survives acros
|
|
|
90
90
|
subsequent command executions:
|
|
91
91
|
|
|
92
92
|
```bash
|
|
93
|
+
# #5548: drop inherited repo-location vars (set inside git hooks) so cwd, not GIT_DIR, picks ROOT.
|
|
94
|
+
unset GIT_ALTERNATE_OBJECT_DIRECTORIES GIT_OBJECT_DIRECTORY GIT_DIR GIT_WORK_TREE GIT_IMPLICIT_WORK_TREE GIT_GRAFT_FILE GIT_INDEX_FILE GIT_NO_REPLACE_OBJECTS GIT_REPLACE_REF_BASE GIT_PREFIX GIT_SHALLOW_FILE GIT_COMMON_DIR
|
|
93
95
|
ROOT=$(git rev-parse --show-toplevel)
|
|
94
96
|
DIRTY_FILE=$(mktemp)
|
|
95
97
|
|
|
@@ -10,6 +10,21 @@ After a GSD update wipes and reinstalls files, this workflow merges user's previ
|
|
|
10
10
|
|
|
11
11
|
<process>
|
|
12
12
|
|
|
13
|
+
## Step 0: Parse interactive mode (#4435)
|
|
14
|
+
|
|
15
|
+
Read the arguments passed after `--reapply`:
|
|
16
|
+
|
|
17
|
+
- no `--interactive` → `INTERACTIVE_MODE=off` (the default; Step 4 runs exactly as before)
|
|
18
|
+
- bare `--interactive` or `--interactive=exceptions` → `INTERACTIVE_MODE=exceptions`
|
|
19
|
+
- `--interactive=all` → `INTERACTIVE_MODE=all`
|
|
20
|
+
- any other `--interactive=<value>` → STOP with: `unknown --interactive value '<value>' (expected exceptions or all)`. Do not reapply anything.
|
|
21
|
+
|
|
22
|
+
When the mode is not `off`, tell the user once, before the first prompt: on the first interactive run
|
|
23
|
+
for this install, `--interactive=all` is recommended, because the recommendations have not yet been
|
|
24
|
+
checked against this install's customizations.
|
|
25
|
+
|
|
26
|
+
**Text mode (`workflow.text_mode: true` in config or `--text` flag):** Set `TEXT_MODE=true` if `--text` is present in the arguments OR `workflow.text_mode` is `true`. When TEXT_MODE is active, replace every `AskUserQuestion` call with a plain-text numbered list and ask the user to type their choice number. This is required for non-Claude runtimes where `AskUserQuestion` is not available.
|
|
27
|
+
|
|
13
28
|
## Step 1: Detect backed-up patches
|
|
14
29
|
|
|
15
30
|
Check for local patches directory:
|
|
@@ -250,6 +265,75 @@ merge paths below, unchanged. If the classifier cannot run at all (e.g. `GSD_HOM
|
|
|
250
265
|
unset), fall back to merging every file as before; never report `Incorporated` without the
|
|
251
266
|
classifier's structured confirmation.
|
|
252
267
|
|
|
268
|
+
### Step 4 interactive review (#4435) — only when `INTERACTIVE_MODE` is not `off`
|
|
269
|
+
|
|
270
|
+
Verbatim matching cannot see an edit that upstream adopted in different words, an edit whose
|
|
271
|
+
surrounding stock text upstream rewrote, or an edit whose anchor lines upstream deleted. Interactive
|
|
272
|
+
mode shows each user edit (hunk) and lets the USER decide. Present and gate: the tool recommends,
|
|
273
|
+
the user decides, and nothing is retired without the user's answer.
|
|
274
|
+
|
|
275
|
+
```bash
|
|
276
|
+
DECISIONS_FILE="$(mktemp "${TMPDIR:-/tmp}/gsd-reapply-decisions.XXXXXX")"
|
|
277
|
+
HUNKS_ARGS=(
|
|
278
|
+
--patches-dir "$PATCHES_DIR"
|
|
279
|
+
--config-dir "$CONFIG_DIR"
|
|
280
|
+
)
|
|
281
|
+
if [ -d "$PRISTINE_DIR" ]; then
|
|
282
|
+
HUNKS_ARGS+=(--pristine-dir "$PRISTINE_DIR")
|
|
283
|
+
fi
|
|
284
|
+
HUNKS_ARGS+=(--hunks --json)
|
|
285
|
+
|
|
286
|
+
# Informational: exits 0. Each hunk carries hunk_hash, kind, stock_lines, user_lines,
|
|
287
|
+
# recommendation (keep | retire | review), reasons, and exception (true/false).
|
|
288
|
+
HUNKS_OUTPUT="$(node "${GSD_HOME}/gsd-core/bin/verify-reapply-patches.cjs" "${HUNKS_ARGS[@]}")"
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
The decisions file lives in a temp directory, never inside `gsd-local-patches/`, because the
|
|
292
|
+
Step 5a gate treats every file in that directory as a backed-up patch.
|
|
293
|
+
|
|
294
|
+
For each file in `HUNKS_OUTPUT.files`, for each hunk:
|
|
295
|
+
|
|
296
|
+
- `INTERACTIVE_MODE=all`: present EVERY hunk.
|
|
297
|
+
- `INTERACTIVE_MODE=exceptions`: present only hunks with `exception: true`. Every other hunk is kept
|
|
298
|
+
without asking. Print one summary line for each:
|
|
299
|
+
`kept without review: {file} hunk {hunk_id}: "{first user line, truncated to 60 chars}"`.
|
|
300
|
+
|
|
301
|
+
Presenting a hunk means asking the user one question (`AskUserQuestion`, or under TEXT_MODE a
|
|
302
|
+
plain-text numbered list the user answers by number) that shows:
|
|
303
|
+
|
|
304
|
+
- **Stock text:** `stock_lines` (or "(none — your lines were inserted)" when empty)
|
|
305
|
+
- **Your text:** `user_lines` (or "(none — you deleted the stock text)" when empty)
|
|
306
|
+
- **Recommendation:** `recommendation`, with `reasons` explained in plain words:
|
|
307
|
+
`already_upstream` (the shipped file already has it), `partially_upstream` (only some of it is
|
|
308
|
+
there), `region_changed_upstream` (upstream also changed this spot, for example by rewording it),
|
|
309
|
+
`anchor_orphaned` (the lines it sat between are gone), `baseline_unconfirmed` (no verified
|
|
310
|
+
pre-update copy, so read the whole file), `diff_too_large` (the file changed too much to split it
|
|
311
|
+
into separate edits)
|
|
312
|
+
- Options: **Keep** (graft it), **Retire** (drop it; keep upstream's text), **Rewrite** (the user
|
|
313
|
+
supplies replacement text, which is applied instead)
|
|
314
|
+
|
|
315
|
+
Write a decision entry only from the user's answer to that hunk's question. Never write `retire` or
|
|
316
|
+
`rewrite` for a hunk the user was not asked about, and never pick an answer on the user's behalf,
|
|
317
|
+
even when the recommendation is `retire`. A hunk kept without review in `exceptions` mode is
|
|
318
|
+
recorded as `keep`. Record each answer in `DECISIONS_FILE` as JSON:
|
|
319
|
+
|
|
320
|
+
```json
|
|
321
|
+
{ "decisions": [ { "file": "<file>", "hunk_hash": "<hunk_hash>", "disposition": "keep|retire|rewrite" } ] }
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
A `rewrite` entry must also carry `"replacement_lines": [...]` — exactly the lines the user supplied.
|
|
325
|
+
The Step 5a gate then requires those lines to be present in the merged file, so a rewrite is verified
|
|
326
|
+
like any kept edit. `keep` and `retire` entries never carry `replacement_lines`.
|
|
327
|
+
|
|
328
|
+
Shell variables do not survive between separate command invocations. Write `INTERACTIVE_MODE` and the
|
|
329
|
+
`DECISIONS_FILE` path down when you set them, and set both again in the same command that runs the
|
|
330
|
+
Step 5a verifier.
|
|
331
|
+
|
|
332
|
+
Then merge each file with the paths below, applying only `keep` hunks and the user's replacement
|
|
333
|
+
text for `rewrite` hunks. A file whose hunks were all retired is left exactly as installed and
|
|
334
|
+
reported `Incorporated` ("Retired by user decision"). A file with some hunks retired is reported
|
|
335
|
+
`Merged` with "{N} hunk(s) retired by user decision".
|
|
336
|
+
|
|
253
337
|
For each file in `backup-meta.json`:
|
|
254
338
|
|
|
255
339
|
1. **Read the backed-up version** (user's modified copy from `gsd-local-patches/`)
|
|
@@ -352,6 +436,11 @@ VERIFY_ARGS=(
|
|
|
352
436
|
if [ -d "$PRISTINE_DIR" ]; then
|
|
353
437
|
VERIFY_ARGS+=(--pristine-dir "$PRISTINE_DIR")
|
|
354
438
|
fi
|
|
439
|
+
# #4435: hunks the user retired or rewrote are excused only through the recorded,
|
|
440
|
+
# hash-bound decisions. A decision whose hunk no longer matches exits 2 (stale).
|
|
441
|
+
if [ "$INTERACTIVE_MODE" != "off" ] && [ -s "$DECISIONS_FILE" ]; then
|
|
442
|
+
VERIFY_ARGS+=(--decisions "$DECISIONS_FILE")
|
|
443
|
+
fi
|
|
355
444
|
VERIFY_ARGS+=(--json)
|
|
356
445
|
|
|
357
446
|
# Capture stdout (the structured JSON report) separately from stderr so that
|
|
@@ -456,6 +545,18 @@ Then re-run /gsd:update --reapply to re-verify.
|
|
|
456
545
|
|
|
457
546
|
Do not proceed to cleanup until the verifier exits 0.
|
|
458
547
|
|
|
548
|
+
**If `VERIFY_STATUS` is 2 and stderr names a `stale decision`** (#4435), a recorded answer no longer
|
|
549
|
+
matches its hunk because the backup or baseline changed after the user saw it. STOP, discard
|
|
550
|
+
`DECISIONS_FILE`, and re-run `/gsd:update --reapply --interactive` so the user decides on the current
|
|
551
|
+
text. Never edit the decisions file to make it match.
|
|
552
|
+
|
|
553
|
+
When decisions were passed, the JSON report carries `decisions.retired` — every hunk excused by a
|
|
554
|
+
user `retire` or `rewrite` decision. Before Step 6, show the user this list (file, hunk id,
|
|
555
|
+
disposition) and ask them to confirm that each one is a decision they made. The gate can prove that a
|
|
556
|
+
decision matches the exact text that was shown, but not who made it, so the user's confirmation of
|
|
557
|
+
this list is the final check. If the user disowns any entry, restore that hunk from the backup and
|
|
558
|
+
re-run Step 5a without it. List each confirmed entry in the Step 7 report.
|
|
559
|
+
|
|
459
560
|
**Only when `VERIFY_STATUS` is 0** (or when all files had zero significant user-added lines, which the verifier reports as `Failures: 0`) may execution continue to gate 5b.
|
|
460
561
|
|
|
461
562
|
### 5b: Hunk Verification Table review (advisory gate, #1999)
|
|
@@ -493,6 +594,8 @@ Do not proceed to cleanup until both gates (5a and 5b) pass.
|
|
|
493
594
|
|
|
494
595
|
## Step 6: Cleanup option
|
|
495
596
|
|
|
597
|
+
If `DECISIONS_FILE` was created (#4435), delete it now: `rm -f "$DECISIONS_FILE"`.
|
|
598
|
+
|
|
496
599
|
Ask user:
|
|
497
600
|
- "Keep patch backups for reference?" → preserve `gsd-local-patches/`
|
|
498
601
|
- "Clean up patch backups?" → remove `gsd-local-patches/` directory
|
|
@@ -520,6 +623,7 @@ Ask user:
|
|
|
520
623
|
- [ ] User modifications identified and merged into new version
|
|
521
624
|
- [ ] Superseded customizations classified `Incorporated` by the deterministic pre-flight classifier (pristine-confirmed) and not re-grafted
|
|
522
625
|
- [ ] Conflicts surfaced to user with both versions shown
|
|
626
|
+
- [ ] With `--interactive`, every presented hunk was decided by the user (never by the agent), retired/rewritten hunks were passed to the Step 5a gate through the hash-bound decisions file, and each retirement is listed in the report
|
|
523
627
|
- [ ] Status reported for each file with summary of what was preserved
|
|
524
628
|
- [ ] Post-merge verification checks each file for dropped hunks and warns if content appears missing
|
|
525
629
|
</success_criteria>
|