gsd-pi 2.80.0-dev.2848d9009 → 2.80.0-dev.2be7caf18
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/README.md +3 -2
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/gsd/auto/loop.js +32 -1
- package/dist/resources/extensions/gsd/auto-prompts.js +13 -1
- package/dist/resources/extensions/gsd/auto.js +7 -2
- package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +27 -20
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +21 -0
- package/dist/resources/extensions/gsd/crash-recovery.js +56 -10
- package/dist/resources/extensions/gsd/guided-flow.js +33 -6
- package/dist/resources/extensions/gsd/migrate/command.js +40 -1
- package/dist/resources/extensions/gsd/migration-auto-check.js +87 -0
- package/dist/resources/extensions/gsd/planning-path-scope.js +26 -0
- package/dist/resources/extensions/gsd/prompts/parallel-research-slices.md +1 -1
- package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/quick-task.md +1 -5
- package/dist/resources/extensions/gsd/prompts/validate-milestone.md +2 -2
- package/dist/resources/extensions/gsd/quick.js +34 -2
- package/dist/resources/extensions/gsd/tools/context-mode-tool-result.js +15 -0
- package/dist/resources/extensions/gsd/tools/exec-search-tool.js +5 -0
- package/dist/resources/extensions/gsd/tools/exec-tool.js +3 -15
- package/dist/resources/extensions/gsd/tools/plan-slice.js +9 -0
- package/dist/resources/extensions/gsd/tools/plan-task.js +9 -0
- package/dist/resources/extensions/gsd/tools/resume-tool.js +5 -0
- package/dist/resources/extensions/gsd/unit-context-composer.js +12 -3
- package/dist/resources/extensions/gsd/unit-runtime.js +11 -0
- package/dist/tsconfig.extensions.tsbuildinfo +1 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +10 -10
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +10 -10
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/package.json +1 -1
- package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.js +22 -17
- package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
- package/packages/mcp-server/src/workflow-tools.test.ts +66 -0
- package/packages/mcp-server/src/workflow-tools.ts +30 -16
- package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js +3 -0
- package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js +3 -1
- package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/compaction/compaction.d.ts +11 -0
- package/packages/pi-coding-agent/dist/core/compaction/compaction.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/compaction/compaction.js +9 -0
- package/packages/pi-coding-agent/dist/core/compaction/compaction.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/compaction-threshold.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/compaction-threshold.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/compaction-threshold.test.js +103 -0
- package/packages/pi-coding-agent/dist/core/compaction-threshold.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/db-snapshot.d.ts +15 -0
- package/packages/pi-coding-agent/dist/core/db-snapshot.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/db-snapshot.js +66 -0
- package/packages/pi-coding-agent/dist/core/db-snapshot.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/db-snapshot.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/db-snapshot.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/db-snapshot.test.js +24 -0
- package/packages/pi-coding-agent/dist/core/db-snapshot.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts +1 -0
- package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.js +3 -0
- package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.test.js +2 -0
- package/packages/pi-coding-agent/dist/core/extensions/runner.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +7 -0
- package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +20 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.js +25 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +3 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +13 -5
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.test.js +53 -0
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +3 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/dist/resources/extensions/memory/storage-safety-guard.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/resources/extensions/memory/storage-safety-guard.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/resources/extensions/memory/storage-safety-guard.test.js +10 -0
- package/packages/pi-coding-agent/dist/resources/extensions/memory/storage-safety-guard.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/resources/extensions/memory/storage.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/resources/extensions/memory/storage.js +3 -2
- package/packages/pi-coding-agent/dist/resources/extensions/memory/storage.js.map +1 -1
- package/packages/pi-coding-agent/src/core/agent-session.ts +3 -0
- package/packages/pi-coding-agent/src/core/chat-controller-ordering.test.ts +3 -1
- package/packages/pi-coding-agent/src/core/compaction/compaction.ts +18 -0
- package/packages/pi-coding-agent/src/core/compaction-threshold.test.ts +121 -0
- package/packages/pi-coding-agent/src/core/db-snapshot.test.ts +32 -0
- package/packages/pi-coding-agent/src/core/db-snapshot.ts +66 -0
- package/packages/pi-coding-agent/src/core/extensions/runner.test.ts +2 -0
- package/packages/pi-coding-agent/src/core/extensions/runner.ts +3 -0
- package/packages/pi-coding-agent/src/core/extensions/types.ts +7 -0
- package/packages/pi-coding-agent/src/core/settings-manager.ts +39 -1
- package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +4 -0
- package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.test.ts +56 -0
- package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +22 -7
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +3 -0
- package/packages/pi-coding-agent/src/resources/extensions/memory/storage-safety-guard.test.ts +14 -0
- package/packages/pi-coding-agent/src/resources/extensions/memory/storage.ts +3 -2
- package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-tui/dist/tui.d.ts.map +1 -1
- package/packages/pi-tui/dist/tui.js +18 -8
- package/packages/pi-tui/dist/tui.js.map +1 -1
- package/packages/pi-tui/src/tui.ts +20 -8
- package/packages/pi-tui/tsconfig.tsbuildinfo +1 -1
- package/src/resources/extensions/gsd/auto/loop.ts +50 -8
- package/src/resources/extensions/gsd/auto-prompts.ts +17 -1
- package/src/resources/extensions/gsd/auto.ts +11 -2
- package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +27 -19
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +22 -0
- package/src/resources/extensions/gsd/crash-recovery.ts +67 -10
- package/src/resources/extensions/gsd/guided-flow.ts +39 -6
- package/src/resources/extensions/gsd/migrate/command.ts +47 -1
- package/src/resources/extensions/gsd/migration-auto-check.ts +129 -0
- package/src/resources/extensions/gsd/planning-path-scope.ts +35 -0
- package/src/resources/extensions/gsd/preferences-types.ts +1 -1
- package/src/resources/extensions/gsd/prompts/parallel-research-slices.md +1 -1
- package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/quick-task.md +1 -5
- package/src/resources/extensions/gsd/prompts/validate-milestone.md +2 -2
- package/src/resources/extensions/gsd/quick.ts +37 -2
- package/src/resources/extensions/gsd/tests/compaction-snapshot.test.ts +11 -0
- package/src/resources/extensions/gsd/tests/crash-handler-secondary.test.ts +55 -0
- package/src/resources/extensions/gsd/tests/crash-recovery-via-db.test.ts +22 -0
- package/src/resources/extensions/gsd/tests/exec-history.test.ts +15 -0
- package/src/resources/extensions/gsd/tests/exec-sandbox.test.ts +65 -0
- package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +48 -0
- package/src/resources/extensions/gsd/tests/migration-auto-check.test.ts +127 -0
- package/src/resources/extensions/gsd/tests/plan-slice.test.ts +50 -0
- package/src/resources/extensions/gsd/tests/plan-task.test.ts +21 -0
- package/src/resources/extensions/gsd/tests/prompt-path-audit.test.ts +40 -0
- package/src/resources/extensions/gsd/tests/quick-external-gsd.test.ts +40 -0
- package/src/resources/extensions/gsd/tests/smart-entry-complete.test.ts +20 -0
- package/src/resources/extensions/gsd/tests/start-auto-detached.test.ts +17 -0
- package/src/resources/extensions/gsd/tests/status-db-open.test.ts +9 -0
- package/src/resources/extensions/gsd/tests/unit-context-composer.test.ts +136 -4
- package/src/resources/extensions/gsd/tests/unit-runtime.test.ts +7 -0
- package/src/resources/extensions/gsd/tools/context-mode-tool-result.ts +25 -0
- package/src/resources/extensions/gsd/tools/exec-search-tool.ts +7 -7
- package/src/resources/extensions/gsd/tools/exec-tool.ts +4 -23
- package/src/resources/extensions/gsd/tools/plan-slice.ts +13 -0
- package/src/resources/extensions/gsd/tools/plan-task.ts +10 -0
- package/src/resources/extensions/gsd/tools/resume-tool.ts +7 -7
- package/src/resources/extensions/gsd/unit-context-composer.ts +19 -4
- package/src/resources/extensions/gsd/unit-runtime.ts +14 -0
- /package/dist/web/standalone/.next/static/{vaQ47E8aNlbbNEDoeME6G → 3EDDd9ULaybSupPoA_vf-}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{vaQ47E8aNlbbNEDoeME6G → 3EDDd9ULaybSupPoA_vf-}/_ssgManifest.js +0 -0
package/README.md
CHANGED
|
@@ -361,7 +361,7 @@ The database is authoritative for milestones, slices, tasks, requirements, decis
|
|
|
361
361
|
|
|
362
362
|
2. **Context pre-loading** — The dispatch prompt includes inlined task plans, slice plans, prior task summaries, dependency summaries, roadmap excerpts, and decisions register. The LLM starts with everything it needs instead of spending tool calls reading files.
|
|
363
363
|
|
|
364
|
-
3. **Context Mode** — Context Mode is enabled by default and gives
|
|
364
|
+
3. **Context Mode** — Context Mode is enabled by default and gives eligible auto-mode units guidance for preserving context. Agents are steered toward `gsd_exec` for noisy scans, builds, tests, and diagnostics; capped stdout/stderr and metadata are saved under `.gsd/exec/` while only a short digest enters the conversation. `gsd_exec_search` lets agents reuse prior runs instead of repeating expensive checks, and `gsd_resume` reads a prior compaction snapshot from `.gsd/last-snapshot.md` when one exists. Opt out with `context_mode.enabled: false` to disable Context Mode guidance, snapshot injection, `gsd_exec`, `gsd_exec_search`, and `gsd_resume`; tune sandbox timeout/output caps and environment forwarding with `context_mode.exec_timeout_ms`, `context_mode.exec_stdout_cap_bytes`, `context_mode.exec_digest_chars`, and `context_mode.exec_env_allowlist`.
|
|
365
365
|
|
|
366
366
|
4. **Git isolation** — When `git.isolation` is set to `worktree` or `branch`, each milestone runs on its own `milestone/<MID>` branch (in a worktree or in-place). All slice work commits sequentially — no branch switching, no merge conflicts. When the milestone completes, it's squash-merged to main as one clean commit. The default is `none` (work on the current branch), configurable via preferences. If `worktree` is configured in a repo with no committed `HEAD`, GSD temporarily behaves as `none` until the first commit exists because git worktrees need a committed start point.
|
|
367
367
|
|
|
@@ -659,10 +659,11 @@ auto_report: true
|
|
|
659
659
|
| `unique_milestone_ids` | Uses unique milestone names to avoid clashes when working in teams of people |
|
|
660
660
|
| `git.isolation` | `none` (default), `worktree`, or `branch` — enable worktree or branch isolation for milestone work. `worktree` requires a committed `HEAD`; zero-commit repos temporarily run as `none` |
|
|
661
661
|
| `git.manage_gitignore` | Set `false` to prevent GSD from modifying `.gitignore` |
|
|
662
|
-
| `context_mode.enabled` | Context Mode is default-on; set `false` to disable `gsd_exec`,
|
|
662
|
+
| `context_mode.enabled` | Context Mode is default-on; set `false` to disable prompt guidance, snapshot injection, `gsd_exec`, `gsd_exec_search`, and `gsd_resume` |
|
|
663
663
|
| `context_mode.exec_timeout_ms` | Timeout for sandboxed `gsd_exec` runs (default: 30000) |
|
|
664
664
|
| `context_mode.exec_stdout_cap_bytes` | Persisted stdout cap for `gsd_exec` output (default: 1048576) |
|
|
665
665
|
| `context_mode.exec_digest_chars` | Trailing stdout characters returned to the agent context (default: 300) |
|
|
666
|
+
| `context_mode.exec_env_allowlist` | Environment variables forwarded to sandboxed `gsd_exec` runs in addition to `PATH` and `HOME` |
|
|
666
667
|
| `verification_commands` | Array of shell commands to run after task execution (e.g., `["npm run lint", "npm run test"]`) |
|
|
667
668
|
| `verification_auto_fix` | Auto-retry on verification failures (default: true) |
|
|
668
669
|
| `verification_max_retries` | Max retries for verification failures (default: 2) |
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
8e6c04d763f9ba00
|
|
@@ -25,6 +25,8 @@ import { normalizeRealPath } from "../paths.js";
|
|
|
25
25
|
import { decideCooldownRecovery, decideDispatchClaim, decideEngineDispatch, decideFinalizeResult, decideInfrastructureError, decideIterationErrorRecovery, decideMemoryPressure, decideModelPolicyBlocked, decideMinRequestInterval, decideWorkflowLoop, formatDispatchExceptionSummary, formatUnhandledDispatchErrorSummary, resolveUnitRequestTimestamp, shouldUseCustomEnginePath, } from "./workflow-kernel.js";
|
|
26
26
|
import { hydrateCustomVerifyRetryCounts, saveCustomVerifyRetryCounts, } from "./custom-verify-retry-store.js";
|
|
27
27
|
import { settleDispatchCompleted, settleDispatchFailed, } from "./workflow-dispatch-ledger.js";
|
|
28
|
+
import { emitOpenUnitEndForUnit } from "../crash-recovery.js";
|
|
29
|
+
import { writeUnitRuntimeRecord } from "../unit-runtime.js";
|
|
28
30
|
import { openDispatchClaim } from "./workflow-dispatch-claim.js";
|
|
29
31
|
import { completeWorkflowIteration } from "./workflow-iteration-completion.js";
|
|
30
32
|
import { createWorkflowJournalReporter } from "./workflow-journal-reporter.js";
|
|
@@ -133,6 +135,24 @@ async function enforceMinRequestInterval(s, prefs) {
|
|
|
133
135
|
await new Promise(r => setTimeout(r, decision.waitMs));
|
|
134
136
|
}
|
|
135
137
|
}
|
|
138
|
+
function closeOutCrashedUnit(s, iterData, err) {
|
|
139
|
+
const summary = formatDispatchExceptionSummary({ error: err });
|
|
140
|
+
try {
|
|
141
|
+
emitOpenUnitEndForUnit(s.basePath, iterData.unitType, iterData.unitId, "cancelled", {
|
|
142
|
+
message: summary,
|
|
143
|
+
category: "unit-exception",
|
|
144
|
+
isTransient: false,
|
|
145
|
+
});
|
|
146
|
+
writeUnitRuntimeRecord(s.basePath, iterData.unitType, iterData.unitId, s.currentUnit?.startedAt ?? Date.now(), {
|
|
147
|
+
phase: "crashed",
|
|
148
|
+
lastProgressAt: Date.now(),
|
|
149
|
+
lastProgressKind: "unit-exception",
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
catch (closeoutErr) {
|
|
153
|
+
logWarning("dispatch", `unit crash closeout failed: ${closeoutErr instanceof Error ? closeoutErr.message : String(closeoutErr)}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
136
156
|
/**
|
|
137
157
|
* Main auto-mode execution loop. Iterates: derive → dispatch → guards →
|
|
138
158
|
* runUnit → finalize → repeat. Exits when s.active becomes false or a
|
|
@@ -392,7 +412,17 @@ export async function autoLoop(ctx, pi, s, deps, options) {
|
|
|
392
412
|
}
|
|
393
413
|
// ── Unit execution (shared with dev path) ──
|
|
394
414
|
await enforceMinRequestInterval(s, prefs);
|
|
395
|
-
|
|
415
|
+
let unitPhaseResult;
|
|
416
|
+
try {
|
|
417
|
+
unitPhaseResult = await runUnitPhaseViaContract(dispatchContract, ic, iterData, loopState, undefined, unitDispatchDeps);
|
|
418
|
+
}
|
|
419
|
+
catch (err) {
|
|
420
|
+
if (err instanceof ModelPolicyDispatchBlockedError) {
|
|
421
|
+
throw err;
|
|
422
|
+
}
|
|
423
|
+
closeOutCrashedUnit(s, iterData, err);
|
|
424
|
+
throw err;
|
|
425
|
+
}
|
|
396
426
|
if (unitPhaseResult.action === "next") {
|
|
397
427
|
const requestTimestamp = resolveUnitRequestTimestamp(unitPhaseResult.data);
|
|
398
428
|
if (requestTimestamp !== undefined)
|
|
@@ -580,6 +610,7 @@ export async function autoLoop(ctx, pi, s, deps, options) {
|
|
|
580
610
|
if (err instanceof ModelPolicyDispatchBlockedError) {
|
|
581
611
|
throw err;
|
|
582
612
|
}
|
|
613
|
+
closeOutCrashedUnit(s, iterData, err);
|
|
583
614
|
dispatchSettled = settleDispatchFailed(dispatchId, formatDispatchExceptionSummary({ error: err }), {
|
|
584
615
|
markFailed: markDispatchFailed,
|
|
585
616
|
logWriteFailure: logDispatchLedgerWriteFailure,
|
|
@@ -21,6 +21,7 @@ import { assertGateCoverage, getGatesForTurn, } from "./gate-registry.js";
|
|
|
21
21
|
import { formatDecisionsCompact, formatRequirementsCompact } from "./structured-data-formatter.js";
|
|
22
22
|
import { readPhaseAnchor, formatAnchorForPrompt } from "./phase-anchor.js";
|
|
23
23
|
import { composeContextModeInstructions, composeInlinedContext } from "./unit-context-composer.js";
|
|
24
|
+
import { readCompactionSnapshot } from "./compaction-snapshot.js";
|
|
24
25
|
import { logWarning } from "./workflow-logger.js";
|
|
25
26
|
import { inlineGraphSubgraph } from "./graph-context.js";
|
|
26
27
|
import { buildExtractionStepsBlock } from "./commands-extract-learnings.js";
|
|
@@ -178,8 +179,19 @@ function renderContextModeForPrompt(unitType, base, renderMode = "standalone") {
|
|
|
178
179
|
renderMode,
|
|
179
180
|
});
|
|
180
181
|
}
|
|
181
|
-
function
|
|
182
|
+
function renderContextModeBlockForPrompt(unitType, base, renderMode = "standalone") {
|
|
182
183
|
const contextMode = renderContextModeForPrompt(unitType, base, renderMode);
|
|
184
|
+
if (!contextMode)
|
|
185
|
+
return "";
|
|
186
|
+
if (renderMode === "nested")
|
|
187
|
+
return contextMode;
|
|
188
|
+
const snapshot = readCompactionSnapshot(base);
|
|
189
|
+
if (!snapshot?.trim())
|
|
190
|
+
return contextMode;
|
|
191
|
+
return `${contextMode}\n\n## Context Snapshot\nSource: \`.gsd/last-snapshot.md\`\n\n${snapshot.trimEnd()}`;
|
|
192
|
+
}
|
|
193
|
+
function prependContextModeToBlock(unitType, base, block, renderMode = "standalone") {
|
|
194
|
+
const contextMode = renderContextModeBlockForPrompt(unitType, base, renderMode);
|
|
183
195
|
if (!contextMode)
|
|
184
196
|
return block;
|
|
185
197
|
if (!block.trim())
|
|
@@ -112,11 +112,12 @@ const STATE_REBUILD_MIN_INTERVAL_MS = 30_000;
|
|
|
112
112
|
* the DB is unavailable (e.g. fresh project before init) we skip registration
|
|
113
113
|
* silently rather than blocking session start.
|
|
114
114
|
*/
|
|
115
|
-
function registerAutoWorkerForSession(session) {
|
|
115
|
+
function registerAutoWorkerForSession(session, projectRootOverride) {
|
|
116
116
|
if (session.workerId)
|
|
117
117
|
return; // already registered (e.g. resume re-runs)
|
|
118
118
|
try {
|
|
119
|
-
const projectRootRealpath = normalizeRealPath(
|
|
119
|
+
const projectRootRealpath = normalizeRealPath(projectRootOverride
|
|
120
|
+
?? session.scope?.workspace.projectRoot
|
|
120
121
|
?? (session.originalBasePath || session.basePath));
|
|
121
122
|
session.workerId = registerAutoWorker({ projectRootRealpath });
|
|
122
123
|
}
|
|
@@ -1631,6 +1632,10 @@ export async function startAuto(ctx, pi, base, verboseMode, options) {
|
|
|
1631
1632
|
lockBase,
|
|
1632
1633
|
buildResolver,
|
|
1633
1634
|
};
|
|
1635
|
+
// Register the worker before bootstrap enters a milestone worktree.
|
|
1636
|
+
// This ensures enterMilestone can claim a lease and seed dispatch claims
|
|
1637
|
+
// for crash-recovery fidelity (#5405).
|
|
1638
|
+
registerAutoWorkerForSession(s, base);
|
|
1634
1639
|
const ready = await bootstrapAutoSession(s, ctx, pi, base, verboseMode, requestedStepMode, bootstrapDeps, freshStartAssessment);
|
|
1635
1640
|
if (!ready)
|
|
1636
1641
|
return;
|
|
@@ -3,21 +3,34 @@
|
|
|
3
3
|
// Exposes the Context Mode runtime tools in-process. Default-on; opt out with
|
|
4
4
|
// `context_mode.enabled: false` in preferences.
|
|
5
5
|
import { Type } from "@sinclair/typebox";
|
|
6
|
+
async function loadContextModePreferences(baseDir) {
|
|
7
|
+
const [{ loadEffectiveGSDPreferences }, { logWarning }] = await Promise.all([
|
|
8
|
+
import("../preferences.js"),
|
|
9
|
+
import("../workflow-logger.js"),
|
|
10
|
+
]);
|
|
11
|
+
try {
|
|
12
|
+
return loadEffectiveGSDPreferences(baseDir)?.preferences ?? null;
|
|
13
|
+
}
|
|
14
|
+
catch (err) {
|
|
15
|
+
logWarning("tool", `Context Mode tool could not load preferences: ${err instanceof Error ? err.message : String(err)}`);
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
6
19
|
export function registerExecTools(pi) {
|
|
7
20
|
pi.registerTool({
|
|
8
21
|
name: "gsd_exec",
|
|
9
22
|
label: "Exec (Sandboxed)",
|
|
10
|
-
description: "Run a short script (bash/node/python) in a subprocess.
|
|
23
|
+
description: "Run a short script (bash/node/python) in a subprocess. Capped stdout/stderr and metadata persist to " +
|
|
11
24
|
".gsd/exec/<id>.{stdout,stderr,meta.json}; only a short digest returns in context. Use " +
|
|
12
25
|
"this instead of reading many files or emitting large tool outputs — e.g. have the script " +
|
|
13
26
|
"count/grep/summarize and log the finding. Enabled by default; opt out via " +
|
|
14
27
|
"preferences.context_mode.enabled=false.",
|
|
15
|
-
promptSnippet: "Run a bash/node/python script in a sandbox;
|
|
28
|
+
promptSnippet: "Run a bash/node/python script in a sandbox; capped output is saved to disk and only a digest returns",
|
|
16
29
|
promptGuidelines: [
|
|
17
30
|
"Prefer gsd_exec for analyses that would otherwise read >3 files or produce large tool output.",
|
|
18
31
|
"Write scripts that log the finding (counts, matches, summaries) rather than raw dumps.",
|
|
19
32
|
"The digest is the last ~300 chars of stdout — size your log output accordingly.",
|
|
20
|
-
"Need
|
|
33
|
+
"Need persisted output? Read the stdout_path returned in details (file on local disk).",
|
|
21
34
|
],
|
|
22
35
|
parameters: Type.Object({
|
|
23
36
|
runtime: Type.Union([Type.Literal("bash"), Type.Literal("node"), Type.Literal("python")], { description: "Interpreter: bash (-c), node (-e), or python3 (-c)." }),
|
|
@@ -30,21 +43,11 @@ export function registerExecTools(pi) {
|
|
|
30
43
|
})),
|
|
31
44
|
}),
|
|
32
45
|
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
import("../preferences.js"),
|
|
36
|
-
import("../workflow-logger.js"),
|
|
37
|
-
]);
|
|
38
|
-
let prefs = null;
|
|
39
|
-
try {
|
|
40
|
-
prefs = loadEffectiveGSDPreferences();
|
|
41
|
-
}
|
|
42
|
-
catch (err) {
|
|
43
|
-
logWarning("tool", `gsd_exec could not load preferences: ${err instanceof Error ? err.message : String(err)}`);
|
|
44
|
-
}
|
|
46
|
+
const { executeGsdExec } = await import("../tools/exec-tool.js");
|
|
47
|
+
const baseDir = process.cwd();
|
|
45
48
|
return executeGsdExec(params, {
|
|
46
|
-
baseDir
|
|
47
|
-
preferences:
|
|
49
|
+
baseDir,
|
|
50
|
+
preferences: await loadContextModePreferences(baseDir),
|
|
48
51
|
});
|
|
49
52
|
},
|
|
50
53
|
});
|
|
@@ -56,7 +59,7 @@ export function registerExecTools(pi) {
|
|
|
56
59
|
promptSnippet: "Search prior gsd_exec runs by substring, runtime, or failing-only filter",
|
|
57
60
|
promptGuidelines: [
|
|
58
61
|
"Use this before re-running an expensive analysis — the prior run's stdout file may still answer.",
|
|
59
|
-
"The preview shows the trailing ~300 chars of stdout; read stdout_path for
|
|
62
|
+
"The preview shows the trailing ~300 chars of stdout; read stdout_path for persisted output.",
|
|
60
63
|
],
|
|
61
64
|
parameters: Type.Object({
|
|
62
65
|
query: Type.Optional(Type.String({ description: "Substring matched against id and purpose (case-insensitive)." })),
|
|
@@ -68,8 +71,10 @@ export function registerExecTools(pi) {
|
|
|
68
71
|
}),
|
|
69
72
|
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
70
73
|
const { executeExecSearch } = await import("../tools/exec-search-tool.js");
|
|
74
|
+
const baseDir = process.cwd();
|
|
71
75
|
return executeExecSearch(params, {
|
|
72
|
-
baseDir
|
|
76
|
+
baseDir,
|
|
77
|
+
preferences: await loadContextModePreferences(baseDir),
|
|
73
78
|
});
|
|
74
79
|
},
|
|
75
80
|
});
|
|
@@ -87,8 +92,10 @@ export function registerExecTools(pi) {
|
|
|
87
92
|
parameters: Type.Object({}),
|
|
88
93
|
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
89
94
|
const { executeResume } = await import("../tools/resume-tool.js");
|
|
95
|
+
const baseDir = process.cwd();
|
|
90
96
|
return executeResume(params, {
|
|
91
|
-
baseDir
|
|
97
|
+
baseDir,
|
|
98
|
+
preferences: await loadContextModePreferences(baseDir),
|
|
92
99
|
});
|
|
93
100
|
},
|
|
94
101
|
});
|
|
@@ -52,6 +52,25 @@ async function applyDisabledModelProviderPolicy(ctx) {
|
|
|
52
52
|
// Non-fatal: keep default provider visibility if preferences cannot be loaded.
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* Bridge `context_management.compaction_threshold_percent` from GSD preferences
|
|
57
|
+
* into the agent's runtime compaction settings (#5475). The preference is
|
|
58
|
+
* validated to (0.5, 0.95) at load time, but defense-in-depth normalization
|
|
59
|
+
* here protects against a stale or hand-edited prefs file. Calling with
|
|
60
|
+
* `undefined` clears any prior override so a removed preference does not leak.
|
|
61
|
+
*/
|
|
62
|
+
async function applyCompactionThresholdOverride(ctx) {
|
|
63
|
+
try {
|
|
64
|
+
const { loadEffectiveGSDPreferences } = await import("../preferences.js");
|
|
65
|
+
const prefs = loadEffectiveGSDPreferences();
|
|
66
|
+
const raw = prefs?.preferences.context_management?.compaction_threshold_percent;
|
|
67
|
+
const value = typeof raw === "number" && Number.isFinite(raw) && raw > 0 && raw < 1 ? raw : undefined;
|
|
68
|
+
ctx.setCompactionThresholdOverride(value);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
// Non-fatal: leave any existing override in place.
|
|
72
|
+
}
|
|
73
|
+
}
|
|
55
74
|
export function resolveNotificationStoreBasePath(cwd = process.cwd()) {
|
|
56
75
|
return resolveWorktreeProjectRoot(cwd);
|
|
57
76
|
}
|
|
@@ -101,6 +120,7 @@ export function registerHooks(pi, ecosystemHandlers) {
|
|
|
101
120
|
await resetAskUserQuestionsTurnCache();
|
|
102
121
|
await syncServiceTierStatus(ctx);
|
|
103
122
|
await applyDisabledModelProviderPolicy(ctx);
|
|
123
|
+
await applyCompactionThresholdOverride(ctx);
|
|
104
124
|
// Skip MCP auto-prep when running inside an auto-worktree (see session_switch below).
|
|
105
125
|
const { isInAutoWorktree } = await import("../auto-worktree.js");
|
|
106
126
|
if (!isInAutoWorktree(process.cwd())) {
|
|
@@ -149,6 +169,7 @@ export function registerHooks(pi, ecosystemHandlers) {
|
|
|
149
169
|
clearDiscussionFlowState(process.cwd());
|
|
150
170
|
await syncServiceTierStatus(ctx);
|
|
151
171
|
await applyDisabledModelProviderPolicy(ctx);
|
|
172
|
+
await applyCompactionThresholdOverride(ctx);
|
|
152
173
|
// Skip MCP auto-prep when running inside an auto-worktree. The worktree
|
|
153
174
|
// already has .mcp.json from createAutoWorktree, and re-running the writer
|
|
154
175
|
// post-chdir rewrites the file mid-run (non-idempotent due to cwd-relative
|
|
@@ -29,6 +29,7 @@ import { _getAdapter, isDbAvailable } from "./gsd-db.js";
|
|
|
29
29
|
import { gsdRoot, normalizeRealPath } from "./paths.js";
|
|
30
30
|
import { atomicWriteSync } from "./atomic-write.js";
|
|
31
31
|
import { effectiveLockFile } from "./session-lock.js";
|
|
32
|
+
import { isInFlightRuntimePhase, listUnitRuntimeRecords } from "./unit-runtime.js";
|
|
32
33
|
const SESSION_FILE_KV_KEY = "session_file";
|
|
33
34
|
function lockPath(basePath) {
|
|
34
35
|
return join(gsdRoot(basePath), effectiveLockFile());
|
|
@@ -72,9 +73,37 @@ function getLatestDispatchForWorker(workerId) {
|
|
|
72
73
|
LIMIT 1`).get({ ":worker_id": workerId });
|
|
73
74
|
return row ?? null;
|
|
74
75
|
}
|
|
75
|
-
function
|
|
76
|
+
function latestInFlightRuntimeRecord(basePath) {
|
|
77
|
+
const records = listUnitRuntimeRecords(basePath).filter((record) => isInFlightRuntimePhase(record.phase));
|
|
78
|
+
if (records.length === 0)
|
|
79
|
+
return null;
|
|
80
|
+
return records.sort((a, b) => {
|
|
81
|
+
const bTime = b.updatedAt || b.startedAt || 0;
|
|
82
|
+
const aTime = a.updatedAt || a.startedAt || 0;
|
|
83
|
+
return bTime - aTime;
|
|
84
|
+
})[0] ?? null;
|
|
85
|
+
}
|
|
86
|
+
function runtimeRecordToLockData(worker, record, sessionFile) {
|
|
87
|
+
const startedAt = Number.isFinite(record.startedAt)
|
|
88
|
+
? new Date(record.startedAt).toISOString()
|
|
89
|
+
: worker.started_at;
|
|
90
|
+
return {
|
|
91
|
+
pid: worker.pid,
|
|
92
|
+
startedAt: worker.started_at,
|
|
93
|
+
unitType: record.unitType,
|
|
94
|
+
unitId: record.unitId,
|
|
95
|
+
unitStartedAt: startedAt,
|
|
96
|
+
sessionFile,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function workerToLockData(basePath, worker) {
|
|
76
100
|
const dispatch = getLatestDispatchForWorker(worker.worker_id);
|
|
77
101
|
const sessionFile = getRuntimeKv("worker", worker.worker_id, SESSION_FILE_KV_KEY) ?? undefined;
|
|
102
|
+
if (!dispatch) {
|
|
103
|
+
const runtimeRecord = latestInFlightRuntimeRecord(basePath);
|
|
104
|
+
if (runtimeRecord)
|
|
105
|
+
return runtimeRecordToLockData(worker, runtimeRecord, sessionFile);
|
|
106
|
+
}
|
|
78
107
|
return {
|
|
79
108
|
pid: worker.pid,
|
|
80
109
|
startedAt: worker.started_at,
|
|
@@ -172,7 +201,7 @@ export function readCrashLock(basePath) {
|
|
|
172
201
|
const projectRoot = normalizeRealPath(basePath);
|
|
173
202
|
const stale = findStaleWorkerForProject(projectRoot);
|
|
174
203
|
if (stale)
|
|
175
|
-
return workerToLockData(stale);
|
|
204
|
+
return workerToLockData(basePath, stale);
|
|
176
205
|
}
|
|
177
206
|
catch {
|
|
178
207
|
// Fall through to the legacy lock-file compatibility path.
|
|
@@ -232,18 +261,32 @@ export function formatCrashInfo(lock) {
|
|
|
232
261
|
export function emitCrashRecoveredUnitEnd(basePath, lock) {
|
|
233
262
|
if (!lock.unitType || !lock.unitId || lock.unitType === "starting")
|
|
234
263
|
return;
|
|
264
|
+
emitOpenUnitEndForUnit(basePath, lock.unitType, lock.unitId, "crash-recovered");
|
|
265
|
+
}
|
|
266
|
+
export function emitOpenUnitEndForUnit(basePath, unitType, unitId, status, errorContext) {
|
|
235
267
|
try {
|
|
236
268
|
const all = queryJournal(basePath);
|
|
237
|
-
const starts = all.filter((e) => e.eventType === "unit-start" &&
|
|
269
|
+
const starts = all.filter((e) => e.eventType === "unit-start" &&
|
|
270
|
+
e.data?.unitType === unitType &&
|
|
271
|
+
e.data?.unitId === unitId);
|
|
238
272
|
if (starts.length === 0)
|
|
239
|
-
return;
|
|
240
|
-
const lastStart =
|
|
273
|
+
return false;
|
|
274
|
+
const lastStart = [...starts].reverse().find((start) => {
|
|
275
|
+
return !all.some((e) => e.eventType === "unit-end" &&
|
|
276
|
+
e.data?.unitType === unitType &&
|
|
277
|
+
e.data?.unitId === unitId &&
|
|
278
|
+
e.causedBy?.flowId === start.flowId &&
|
|
279
|
+
e.causedBy?.seq === start.seq);
|
|
280
|
+
});
|
|
281
|
+
if (!lastStart)
|
|
282
|
+
return false;
|
|
241
283
|
const alreadyClosed = all.some((e) => e.eventType === "unit-end" &&
|
|
242
|
-
e.data?.
|
|
284
|
+
e.data?.unitType === unitType &&
|
|
285
|
+
e.data?.unitId === unitId &&
|
|
243
286
|
e.causedBy?.flowId === lastStart.flowId &&
|
|
244
287
|
e.causedBy?.seq === lastStart.seq);
|
|
245
288
|
if (alreadyClosed)
|
|
246
|
-
return;
|
|
289
|
+
return false;
|
|
247
290
|
const maxSeq = all
|
|
248
291
|
.filter((e) => e.flowId === lastStart.flowId)
|
|
249
292
|
.reduce((max, e) => Math.max(max, e.seq), lastStart.seq);
|
|
@@ -253,16 +296,19 @@ export function emitCrashRecoveredUnitEnd(basePath, lock) {
|
|
|
253
296
|
seq: maxSeq + 1,
|
|
254
297
|
eventType: "unit-end",
|
|
255
298
|
data: {
|
|
256
|
-
unitType
|
|
257
|
-
unitId
|
|
258
|
-
status
|
|
299
|
+
unitType,
|
|
300
|
+
unitId,
|
|
301
|
+
status,
|
|
259
302
|
artifactVerified: false,
|
|
303
|
+
...(errorContext ? { errorContext } : {}),
|
|
260
304
|
},
|
|
261
305
|
causedBy: { flowId: lastStart.flowId, seq: lastStart.seq },
|
|
262
306
|
});
|
|
307
|
+
return true;
|
|
263
308
|
}
|
|
264
309
|
catch {
|
|
265
310
|
// Never throw from crash recovery path.
|
|
311
|
+
return false;
|
|
266
312
|
}
|
|
267
313
|
}
|
|
268
314
|
/**
|
|
@@ -16,7 +16,7 @@ import { invalidateAllCaches } from "./cache.js";
|
|
|
16
16
|
import { startAutoDetached } from "./auto.js";
|
|
17
17
|
import { clearLock } from "./crash-recovery.js";
|
|
18
18
|
import { assessInterruptedSession, formatInterruptedSessionRunningMessage, formatInterruptedSessionSummary, } from "./interrupted-session.js";
|
|
19
|
-
import { listUnitRuntimeRecords, clearUnitRuntimeRecord } from "./unit-runtime.js";
|
|
19
|
+
import { listUnitRuntimeRecords, clearUnitRuntimeRecord, isInFlightRuntimePhase } from "./unit-runtime.js";
|
|
20
20
|
import { resolveExpectedArtifactPath } from "./auto.js";
|
|
21
21
|
import { gsdHome } from "./gsd-home.js";
|
|
22
22
|
import { gsdRoot, milestonesDir, resolveMilestoneFile, resolveMilestonePath, resolveSliceFile, resolveSlicePath, resolveGsdRootFile, relGsdRootFile, relMilestoneFile, relSliceFile, clearPathCache, } from "./paths.js";
|
|
@@ -71,6 +71,19 @@ export function verifyExpectedArtifactForScope(scope, unitType, unitId) {
|
|
|
71
71
|
export function resolveExpectedArtifactPathForScope(scope, unitType, unitId) {
|
|
72
72
|
return resolveExpectedArtifactPath(unitType, unitId, scope.workspace.projectRoot);
|
|
73
73
|
}
|
|
74
|
+
async function runQuickTaskChoice(ctx, pi) {
|
|
75
|
+
if (!ctx.hasUI) {
|
|
76
|
+
ctx.ui.notify("Run /gsd quick <task> for small bounded work, or /gsd do <task> for natural-language routing.", "info");
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const task = (await ctx.ui.input("Quick task", "Describe the small task to run with /gsd quick"))?.trim();
|
|
80
|
+
if (!task) {
|
|
81
|
+
ctx.ui.notify("Quick task cancelled.", "info");
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const { handleQuick } = await import("./quick.js");
|
|
85
|
+
await handleQuick(task, ctx, pi);
|
|
86
|
+
}
|
|
74
87
|
/**
|
|
75
88
|
* Scope-based overload of isGhostMilestone.
|
|
76
89
|
* Binds basePath and milestoneId from the scope, ensuring path resolution
|
|
@@ -1469,8 +1482,8 @@ function selfHealRuntimeRecords(basePath, ctx) {
|
|
|
1469
1482
|
cleared++;
|
|
1470
1483
|
continue;
|
|
1471
1484
|
}
|
|
1472
|
-
// Clear records stuck in
|
|
1473
|
-
if (phase
|
|
1485
|
+
// Clear records stuck in an in-flight phase (process died mid-unit).
|
|
1486
|
+
if (isInFlightRuntimePhase(phase)) {
|
|
1474
1487
|
clearUnitRuntimeRecord(basePath, unitType, unitId);
|
|
1475
1488
|
cleared++;
|
|
1476
1489
|
}
|
|
@@ -1695,6 +1708,20 @@ export async function showSmartEntry(ctx, pi, basePath, options) {
|
|
|
1695
1708
|
return;
|
|
1696
1709
|
}
|
|
1697
1710
|
}
|
|
1711
|
+
if (interrupted.classification !== "recoverable") {
|
|
1712
|
+
try {
|
|
1713
|
+
const { autoImportMarkdownHierarchyIfDbMismatch } = await import("./migration-auto-check.js");
|
|
1714
|
+
const result = await autoImportMarkdownHierarchyIfDbMismatch(basePath);
|
|
1715
|
+
if (result.action === "imported") {
|
|
1716
|
+
ctx.ui.notify(`Recovered migrated planning state into gsd.db (${result.reason}): ${result.afterDb.milestones} milestone(s), ${result.afterDb.slices} slice(s), ${result.afterDb.tasks} task(s).`, "info");
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
catch (err) {
|
|
1720
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1721
|
+
ctx.ui.notify(`GSD could not auto-import existing planning state into gsd.db: ${message}`, "warning");
|
|
1722
|
+
logWarning("guided", `planning state auto-import failed: ${message}`, { file: "guided-flow.ts" });
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1698
1725
|
// Always derive from the project root — the assessment may have derived
|
|
1699
1726
|
// state from a worktree path that was cleaned up in the stale branch above.
|
|
1700
1727
|
const state = await deriveState(basePath);
|
|
@@ -1795,7 +1822,7 @@ export async function showSmartEntry(ctx, pi, basePath, options) {
|
|
|
1795
1822
|
notYetMessage: "Run /gsd when ready.",
|
|
1796
1823
|
});
|
|
1797
1824
|
if (choice === "quick_task") {
|
|
1798
|
-
ctx
|
|
1825
|
+
await runQuickTaskChoice(ctx, pi);
|
|
1799
1826
|
}
|
|
1800
1827
|
else if (choice === "new_milestone") {
|
|
1801
1828
|
setPendingAutoStart(basePath, { ctx, pi, basePath, milestoneId: nextId, step: stepMode });
|
|
@@ -1837,7 +1864,7 @@ export async function showSmartEntry(ctx, pi, basePath, options) {
|
|
|
1837
1864
|
notYetMessage: "Run /gsd when ready.",
|
|
1838
1865
|
});
|
|
1839
1866
|
if (choice === "quick_task") {
|
|
1840
|
-
ctx
|
|
1867
|
+
await runQuickTaskChoice(ctx, pi);
|
|
1841
1868
|
}
|
|
1842
1869
|
else if (choice === "new_milestone") {
|
|
1843
1870
|
const milestoneIds = findMilestoneIds(basePath);
|
|
@@ -1968,7 +1995,7 @@ export async function showSmartEntry(ctx, pi, basePath, options) {
|
|
|
1968
1995
|
notYetMessage: "Run /gsd when ready.",
|
|
1969
1996
|
});
|
|
1970
1997
|
if (choice === "quick_task") {
|
|
1971
|
-
ctx
|
|
1998
|
+
await runQuickTaskChoice(ctx, pi);
|
|
1972
1999
|
}
|
|
1973
2000
|
else if (choice === "plan") {
|
|
1974
2001
|
setPendingAutoStart(basePath, { ctx, pi, basePath, milestoneId, step: stepMode });
|
|
@@ -15,6 +15,43 @@ import { fileURLToPath } from "node:url";
|
|
|
15
15
|
import { showNextAction } from "../../shared/tui.js";
|
|
16
16
|
import { validatePlanningDirectory, parsePlanningDirectory, transformToGSD, generatePreview, writeGSDDirectory, } from "./index.js";
|
|
17
17
|
import { homedir } from "node:os";
|
|
18
|
+
import { ensureDbOpen } from "../bootstrap/dynamic-tools.js";
|
|
19
|
+
import { clearEngineHierarchy, transaction } from "../gsd-db.js";
|
|
20
|
+
import { migrateFromMarkdown } from "../md-importer.js";
|
|
21
|
+
import { invalidateStateCache } from "../state.js";
|
|
22
|
+
function assertMigrationImportMatchesPreview(imported, preview) {
|
|
23
|
+
const mismatches = [];
|
|
24
|
+
if (imported.hierarchy.milestones !== preview.milestoneCount) {
|
|
25
|
+
mismatches.push(`milestones ${imported.hierarchy.milestones}/${preview.milestoneCount}`);
|
|
26
|
+
}
|
|
27
|
+
if (imported.hierarchy.slices !== preview.totalSlices) {
|
|
28
|
+
mismatches.push(`slices ${imported.hierarchy.slices}/${preview.totalSlices}`);
|
|
29
|
+
}
|
|
30
|
+
if (imported.hierarchy.tasks !== preview.totalTasks) {
|
|
31
|
+
mismatches.push(`tasks ${imported.hierarchy.tasks}/${preview.totalTasks}`);
|
|
32
|
+
}
|
|
33
|
+
if (imported.requirements !== preview.requirements.total) {
|
|
34
|
+
mismatches.push(`requirements ${imported.requirements}/${preview.requirements.total}`);
|
|
35
|
+
}
|
|
36
|
+
if (mismatches.length > 0) {
|
|
37
|
+
throw new Error(`migration DB import verification failed: ${mismatches.join(", ")}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export async function importWrittenMigrationToDb(basePath, preview) {
|
|
41
|
+
const opened = await ensureDbOpen(basePath);
|
|
42
|
+
if (!opened) {
|
|
43
|
+
throw new Error(`failed to open or create the GSD database at ${basePath}`);
|
|
44
|
+
}
|
|
45
|
+
const counts = transaction(() => {
|
|
46
|
+
clearEngineHierarchy();
|
|
47
|
+
const imported = migrateFromMarkdown(basePath);
|
|
48
|
+
if (preview)
|
|
49
|
+
assertMigrationImportMatchesPreview(imported, preview);
|
|
50
|
+
return imported;
|
|
51
|
+
});
|
|
52
|
+
invalidateStateCache();
|
|
53
|
+
return counts;
|
|
54
|
+
}
|
|
18
55
|
/** Format preview stats for embedding in the review prompt. */
|
|
19
56
|
function formatPreviewStats(preview) {
|
|
20
57
|
const lines = [
|
|
@@ -126,12 +163,14 @@ export async function handleMigrate(args, ctx, pi) {
|
|
|
126
163
|
ctx.ui.notify("Writing .gsd directory…", "info");
|
|
127
164
|
const result = await writeGSDDirectory(project, process.cwd());
|
|
128
165
|
const gsdPath = gsdRoot(process.cwd());
|
|
129
|
-
|
|
166
|
+
const imported = await importWrittenMigrationToDb(process.cwd(), preview);
|
|
167
|
+
ctx.ui.notify(`✓ Migration complete — ${result.paths.length} file(s) written to .gsd/ and ${imported.hierarchy.milestones}M/${imported.hierarchy.slices}S/${imported.hierarchy.tasks}T imported to the database`, "info");
|
|
130
168
|
// ── Post-write review offer ────────────────────────────────────────────────
|
|
131
169
|
const reviewChoice = await showNextAction(ctx, {
|
|
132
170
|
title: "Migration written",
|
|
133
171
|
summary: [
|
|
134
172
|
`${result.paths.length} files written to .gsd/`,
|
|
173
|
+
`${imported.hierarchy.milestones} milestone(s), ${imported.hierarchy.slices} slice(s), and ${imported.hierarchy.tasks} task(s) imported to gsd.db`,
|
|
135
174
|
"",
|
|
136
175
|
"The agent can now review the migrated output against GSD-2 standards —",
|
|
137
176
|
"checking structure, content quality, deriveState() round-trip, and",
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { ensureDbOpen } from "./bootstrap/dynamic-tools.js";
|
|
3
|
+
import { clearEngineHierarchy, getAllMilestones, getMilestoneSlices, getSliceTasks, isDbAvailable, transaction, } from "./gsd-db.js";
|
|
4
|
+
import { migrateHierarchyToDb } from "./md-importer.js";
|
|
5
|
+
import { parsePlan, parseRoadmap } from "./parsers-legacy.js";
|
|
6
|
+
import { milestonesDir, resolveMilestoneFile, resolveSliceFile, } from "./paths.js";
|
|
7
|
+
import { invalidateStateCache } from "./state.js";
|
|
8
|
+
function zeroCounts() {
|
|
9
|
+
return { milestones: 0, slices: 0, tasks: 0 };
|
|
10
|
+
}
|
|
11
|
+
function sameCounts(a, b) {
|
|
12
|
+
return a.milestones === b.milestones && a.slices === b.slices && a.tasks === b.tasks;
|
|
13
|
+
}
|
|
14
|
+
export function countMarkdownHierarchy(basePath) {
|
|
15
|
+
const root = milestonesDir(basePath);
|
|
16
|
+
if (!existsSync(root))
|
|
17
|
+
return zeroCounts();
|
|
18
|
+
const counts = zeroCounts();
|
|
19
|
+
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
|
20
|
+
if (!entry.isDirectory() || !/^M\d+/.test(entry.name))
|
|
21
|
+
continue;
|
|
22
|
+
counts.milestones++;
|
|
23
|
+
const roadmapPath = resolveMilestoneFile(basePath, entry.name, "ROADMAP");
|
|
24
|
+
if (!roadmapPath || !existsSync(roadmapPath))
|
|
25
|
+
continue;
|
|
26
|
+
const roadmap = parseRoadmap(readFileSync(roadmapPath, "utf-8"));
|
|
27
|
+
counts.slices += roadmap.slices.length;
|
|
28
|
+
for (const slice of roadmap.slices) {
|
|
29
|
+
const planPath = resolveSliceFile(basePath, entry.name, slice.id, "PLAN");
|
|
30
|
+
if (!planPath || !existsSync(planPath))
|
|
31
|
+
continue;
|
|
32
|
+
const plan = parsePlan(readFileSync(planPath, "utf-8"));
|
|
33
|
+
counts.tasks += plan.tasks.length;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return counts;
|
|
37
|
+
}
|
|
38
|
+
export function countDbHierarchy() {
|
|
39
|
+
if (!isDbAvailable())
|
|
40
|
+
return zeroCounts();
|
|
41
|
+
const counts = zeroCounts();
|
|
42
|
+
const milestones = getAllMilestones();
|
|
43
|
+
counts.milestones = milestones.length;
|
|
44
|
+
for (const milestone of milestones) {
|
|
45
|
+
const slices = getMilestoneSlices(milestone.id);
|
|
46
|
+
counts.slices += slices.length;
|
|
47
|
+
for (const slice of slices) {
|
|
48
|
+
counts.tasks += getSliceTasks(milestone.id, slice.id).length;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return counts;
|
|
52
|
+
}
|
|
53
|
+
export async function autoImportMarkdownHierarchyIfDbMismatch(basePath) {
|
|
54
|
+
const markdown = countMarkdownHierarchy(basePath);
|
|
55
|
+
if (sameCounts(markdown, zeroCounts())) {
|
|
56
|
+
return {
|
|
57
|
+
action: "none",
|
|
58
|
+
reason: "no-markdown",
|
|
59
|
+
markdown,
|
|
60
|
+
beforeDb: zeroCounts(),
|
|
61
|
+
afterDb: zeroCounts(),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
const opened = await ensureDbOpen(basePath);
|
|
65
|
+
if (!opened || !isDbAvailable()) {
|
|
66
|
+
throw new Error(`failed to open or create the GSD database at ${basePath}`);
|
|
67
|
+
}
|
|
68
|
+
const beforeDb = countDbHierarchy();
|
|
69
|
+
if (sameCounts(markdown, beforeDb)) {
|
|
70
|
+
return { action: "none", reason: "in-sync", markdown, beforeDb, afterDb: beforeDb };
|
|
71
|
+
}
|
|
72
|
+
const reason = sameCounts(beforeDb, zeroCounts()) ? "db-empty" : "count-mismatch";
|
|
73
|
+
const imported = transaction(() => {
|
|
74
|
+
clearEngineHierarchy();
|
|
75
|
+
return migrateHierarchyToDb(basePath);
|
|
76
|
+
});
|
|
77
|
+
invalidateStateCache();
|
|
78
|
+
const afterDb = {
|
|
79
|
+
milestones: imported.milestones,
|
|
80
|
+
slices: imported.slices,
|
|
81
|
+
tasks: imported.tasks,
|
|
82
|
+
};
|
|
83
|
+
if (!sameCounts(markdown, afterDb)) {
|
|
84
|
+
throw new Error(`migration auto-import verification failed: markdown ${markdown.milestones}M/${markdown.slices}S/${markdown.tasks}T, db ${afterDb.milestones}M/${afterDb.slices}S/${afterDb.tasks}T`);
|
|
85
|
+
}
|
|
86
|
+
return { action: "imported", reason, markdown, beforeDb, afterDb };
|
|
87
|
+
}
|