@opengsd/gsd-pi 1.3.0-dev.72e3af2a → 1.3.0-dev.738160b9
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 +1 -1
- package/dist/cli.js +17 -4
- package/dist/headless-answers.js +1 -1
- package/dist/headless-ui.d.ts +1 -1
- package/dist/headless-ui.js +1 -1
- package/dist/headless.js +1 -1
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/claude-code-cli/models.js +9 -0
- package/dist/resources/extensions/gsd/auto/session.js +8 -0
- package/dist/resources/extensions/gsd/auto/verification-retry-policy.js +1 -1
- package/dist/resources/extensions/gsd/auto-budget.js +5 -1
- package/dist/resources/extensions/gsd/auto-dispatch.js +14 -1
- package/dist/resources/extensions/gsd/auto-post-unit.js +1 -1
- package/dist/resources/extensions/gsd/auto-prompts.js +3 -3
- package/dist/resources/extensions/gsd/auto-recovery.js +3 -3
- package/dist/resources/extensions/gsd/auto-verification.js +5 -2
- package/dist/resources/extensions/gsd/auto.js +19 -10
- package/dist/resources/extensions/gsd/bootstrap/core-session-tools.js +1 -0
- package/dist/resources/extensions/gsd/bootstrap/db-tools.js +5 -4
- package/dist/resources/extensions/gsd/bootstrap/register-extension.js +2 -0
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +31 -2
- package/dist/resources/extensions/gsd/bootstrap/tool-call-loop-guard.js +32 -4
- package/dist/resources/extensions/gsd/closeout-consistency-gate.js +3 -1
- package/dist/resources/extensions/gsd/commands/handlers/auto.js +1 -1
- package/dist/resources/extensions/gsd/commands-prefs-wizard.js +1 -1
- package/dist/resources/extensions/gsd/commands-usage.js +1 -1
- package/dist/resources/extensions/gsd/db/engine.js +4 -1
- package/dist/resources/extensions/gsd/db-memory-fts-schema.js +34 -0
- package/dist/resources/extensions/gsd/delegation-policy.js +1 -1
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +2 -2
- package/dist/resources/extensions/gsd/doctor-engine-checks.js +192 -4
- package/dist/resources/extensions/gsd/doctor-runtime-checks.js +3 -0
- package/dist/resources/extensions/gsd/exec-sandbox.js +8 -2
- package/dist/resources/extensions/gsd/flat-phase-migration.js +45 -0
- package/dist/resources/extensions/gsd/git-service.js +57 -0
- package/dist/resources/extensions/gsd/gitignore.js +4 -1
- package/dist/resources/extensions/gsd/json-persistence.js +20 -0
- package/dist/resources/extensions/gsd/knowledge-backfill.js +18 -11
- package/dist/resources/extensions/gsd/memory-backfill.js +28 -21
- package/dist/resources/extensions/gsd/metrics.js +4 -4
- package/dist/resources/extensions/gsd/migrate-external.js +11 -2
- package/dist/resources/extensions/gsd/orphan-stash-audit.js +92 -16
- package/dist/resources/extensions/gsd/prompts/plan-milestone.md +2 -2
- package/dist/resources/extensions/gsd/prompts/plan-slice.md +3 -3
- package/dist/resources/extensions/gsd/root-write-leak-guard.js +7 -1
- package/dist/resources/extensions/gsd/safety/destructive-guard.js +8 -1
- package/dist/resources/extensions/gsd/safety/evidence-collector.js +16 -1
- package/dist/resources/extensions/gsd/safety/file-change-validator.js +5 -2
- package/dist/resources/extensions/gsd/tools/complete-milestone.js +4 -2
- package/dist/resources/extensions/gsd/tools/plan-slice.js +59 -44
- package/dist/resources/extensions/gsd/tools/plan-task.js +28 -5
- package/dist/resources/extensions/gsd/undo.js +43 -6
- package/dist/resources/extensions/gsd/unit-registry.js +4 -2
- package/dist/resources/extensions/gsd/verification-gate.js +7 -0
- package/dist/resources/extensions/gsd/workflow-manifest.js +126 -13
- 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 +9 -9
- 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 +9 -9
- package/dist/web/standalone/.next/server/chunks/8357.js +1 -1
- 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/cloud-mcp-gateway/package.json +2 -2
- package/packages/contracts/package.json +1 -1
- package/packages/daemon/package.json +4 -4
- package/packages/gsd-agent-core/package.json +5 -5
- package/packages/gsd-agent-modes/dist/modes/rpc/rpc-client.d.ts +2 -0
- package/packages/gsd-agent-modes/dist/modes/rpc/rpc-client.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/rpc/rpc-client.js +23 -5
- package/packages/gsd-agent-modes/dist/modes/rpc/rpc-client.js.map +1 -1
- package/packages/gsd-agent-modes/package.json +7 -7
- package/packages/mcp-server/dist/workflow-tools.js +1 -1
- package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
- package/packages/mcp-server/package.json +4 -4
- package/packages/native/package.json +1 -1
- package/packages/pi-agent-core/dist/agent-loop.js +3 -2
- package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
- package/packages/pi-agent-core/package.json +1 -1
- package/packages/pi-ai/dist/models.generated.d.ts +46 -0
- package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
- package/packages/pi-ai/dist/models.generated.js +38 -0
- package/packages/pi-ai/dist/models.generated.js.map +1 -1
- package/packages/pi-ai/package.json +1 -1
- package/packages/pi-coding-agent/package.json +7 -7
- package/packages/pi-tui/package.json +2 -2
- package/packages/rpc-client/package.json +2 -2
- package/pkg/package.json +1 -1
- package/src/resources/extensions/claude-code-cli/models.ts +9 -0
- package/src/resources/extensions/claude-code-cli/tests/index.test.ts +14 -0
- package/src/resources/extensions/gsd/auto/loop-deps.ts +1 -0
- package/src/resources/extensions/gsd/auto/session.ts +9 -0
- package/src/resources/extensions/gsd/auto/verification-retry-policy.ts +3 -1
- package/src/resources/extensions/gsd/auto-budget.ts +12 -1
- package/src/resources/extensions/gsd/auto-dispatch.ts +14 -1
- package/src/resources/extensions/gsd/auto-post-unit.ts +1 -1
- package/src/resources/extensions/gsd/auto-prompts.ts +3 -3
- package/src/resources/extensions/gsd/auto-recovery.ts +3 -3
- package/src/resources/extensions/gsd/auto-verification.ts +5 -1
- package/src/resources/extensions/gsd/auto.ts +23 -8
- package/src/resources/extensions/gsd/bootstrap/core-session-tools.ts +1 -0
- package/src/resources/extensions/gsd/bootstrap/db-tools.ts +5 -4
- package/src/resources/extensions/gsd/bootstrap/register-extension.ts +2 -0
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +41 -2
- package/src/resources/extensions/gsd/bootstrap/tool-call-loop-guard.ts +34 -4
- package/src/resources/extensions/gsd/closeout-consistency-gate.ts +2 -1
- package/src/resources/extensions/gsd/commands/handlers/auto.ts +1 -1
- package/src/resources/extensions/gsd/commands-prefs-wizard.ts +1 -1
- package/src/resources/extensions/gsd/commands-usage.ts +1 -1
- package/src/resources/extensions/gsd/db/engine.ts +8 -1
- package/src/resources/extensions/gsd/db-memory-fts-schema.ts +45 -0
- package/src/resources/extensions/gsd/delegation-policy.ts +1 -1
- package/src/resources/extensions/gsd/docs/preferences-reference.md +2 -2
- package/src/resources/extensions/gsd/doctor-engine-checks.ts +233 -5
- package/src/resources/extensions/gsd/doctor-runtime-checks.ts +3 -0
- package/src/resources/extensions/gsd/doctor-types.ts +3 -0
- package/src/resources/extensions/gsd/exec-sandbox.ts +7 -2
- package/src/resources/extensions/gsd/flat-phase-migration.ts +44 -0
- package/src/resources/extensions/gsd/git-service.ts +64 -0
- package/src/resources/extensions/gsd/gitignore.ts +4 -1
- package/src/resources/extensions/gsd/json-persistence.ts +20 -0
- package/src/resources/extensions/gsd/knowledge-backfill.ts +16 -10
- package/src/resources/extensions/gsd/memory-backfill.ts +26 -20
- package/src/resources/extensions/gsd/metrics.ts +4 -4
- package/src/resources/extensions/gsd/migrate-external.ts +8 -2
- package/src/resources/extensions/gsd/orphan-stash-audit.ts +108 -20
- package/src/resources/extensions/gsd/prompts/plan-milestone.md +2 -2
- package/src/resources/extensions/gsd/prompts/plan-slice.md +3 -3
- package/src/resources/extensions/gsd/root-write-leak-guard.ts +13 -1
- package/src/resources/extensions/gsd/safety/destructive-guard.ts +9 -1
- package/src/resources/extensions/gsd/safety/evidence-collector.ts +16 -1
- package/src/resources/extensions/gsd/safety/file-change-validator.ts +8 -2
- package/src/resources/extensions/gsd/tests/artifact-retry-cap.test.ts +4 -3
- package/src/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +14 -5
- package/src/resources/extensions/gsd/tests/auto-pause-double-entry-guard.test.ts +36 -0
- package/src/resources/extensions/gsd/tests/auto-paused-ui-cleanup.test.ts +182 -4
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +35 -0
- package/src/resources/extensions/gsd/tests/auto-retry-mcp-churn-fixes.test.ts +30 -0
- package/src/resources/extensions/gsd/tests/complete-milestone.test.ts +28 -0
- package/src/resources/extensions/gsd/tests/complete-slice-reopen-handoff.test.ts +69 -0
- package/src/resources/extensions/gsd/tests/db-memory-fts-schema.test.ts +58 -3
- package/src/resources/extensions/gsd/tests/decisions-projection-from-memories.test.ts +51 -0
- package/src/resources/extensions/gsd/tests/destructive-guard.test.ts +44 -0
- package/src/resources/extensions/gsd/tests/dispatch-missing-task-plans.test.ts +143 -0
- package/src/resources/extensions/gsd/tests/doctor-scope-db-unavailable.test.ts +239 -2
- package/src/resources/extensions/gsd/tests/exec-sandbox.test.ts +16 -0
- package/src/resources/extensions/gsd/tests/file-change-validator.test.ts +40 -0
- package/src/resources/extensions/gsd/tests/flat-phase-migration.test.ts +37 -2
- package/src/resources/extensions/gsd/tests/gitignore-bg-shell-runtime.test.ts +4 -0
- package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +11 -2
- package/src/resources/extensions/gsd/tests/json-persistence-atomic.test.ts +47 -0
- package/src/resources/extensions/gsd/tests/knowledge-backfill-projection.test.ts +35 -0
- package/src/resources/extensions/gsd/tests/memory-store.test.ts +48 -0
- package/src/resources/extensions/gsd/tests/merge-closeout-consistency-gate.test.ts +31 -1
- package/src/resources/extensions/gsd/tests/metrics-prune-cache-invalidation.test.ts +6 -0
- package/src/resources/extensions/gsd/tests/metrics.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/migrate-external-worktree.test.ts +57 -0
- package/src/resources/extensions/gsd/tests/orphan-stash-audit.test.ts +91 -7
- package/src/resources/extensions/gsd/tests/parsers-legacy-importers.test.ts +2 -0
- package/src/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +10 -0
- package/src/resources/extensions/gsd/tests/plan-slice.test.ts +103 -4
- package/src/resources/extensions/gsd/tests/plan-task.test.ts +44 -1
- package/src/resources/extensions/gsd/tests/post-mutation-hook.test.ts +4 -2
- package/src/resources/extensions/gsd/tests/prompt-budget-enforcement.test.ts +58 -0
- package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +8 -6
- package/src/resources/extensions/gsd/tests/register-hooks-loop-guard-auto.test.ts +103 -0
- package/src/resources/extensions/gsd/tests/root-write-leak-guard.test.ts +20 -1
- package/src/resources/extensions/gsd/tests/runtime-invariant-modules.test.ts +8 -3
- package/src/resources/extensions/gsd/tests/tool-call-loop-guard.test.ts +62 -5
- package/src/resources/extensions/gsd/tests/tool-param-optionality.test.ts +6 -3
- package/src/resources/extensions/gsd/tests/undo.test.ts +74 -0
- package/src/resources/extensions/gsd/tests/uok-gitops-turn-action.test.ts +38 -0
- package/src/resources/extensions/gsd/tests/verification-retry-policy.test.ts +69 -4
- package/src/resources/extensions/gsd/tests/workflow-manifest.test.ts +96 -15
- package/src/resources/extensions/gsd/tests/workflow-mcp-preflight.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +8 -4
- package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +6 -5
- package/src/resources/extensions/gsd/tools/complete-milestone.ts +3 -2
- package/src/resources/extensions/gsd/tools/plan-slice.ts +60 -47
- package/src/resources/extensions/gsd/tools/plan-task.ts +30 -5
- package/src/resources/extensions/gsd/undo.ts +47 -6
- package/src/resources/extensions/gsd/unit-registry.ts +4 -2
- package/src/resources/extensions/gsd/verification-gate.ts +8 -0
- package/src/resources/extensions/gsd/workflow-manifest.ts +141 -7
- /package/dist/web/standalone/.next/static/{O7xDYXO0r4zFhIzY1hrWV → 2A_WqqCS-EoYZxN9_v3d2}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{O7xDYXO0r4zFhIzY1hrWV → 2A_WqqCS-EoYZxN9_v3d2}/_ssgManifest.js +0 -0
|
@@ -4,7 +4,7 @@ import { mkdtempSync, mkdirSync, rmSync, readFileSync, existsSync, writeFileSync
|
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { tmpdir } from 'node:os';
|
|
6
6
|
|
|
7
|
-
import { openDatabase, closeDatabase, insertMilestone, insertSlice, insertTask, getTask } from '../gsd-db.ts';
|
|
7
|
+
import { openDatabase, closeDatabase, insertMilestone, insertSlice, insertTask, getSlice, getTask, getGateResults } from '../gsd-db.ts';
|
|
8
8
|
import { handlePlanTask } from '../tools/plan-task.ts';
|
|
9
9
|
import { parseTaskPlanFile } from '../files.ts';
|
|
10
10
|
|
|
@@ -65,6 +65,25 @@ test('handlePlanTask writes planning state and renders task plan', async () => {
|
|
|
65
65
|
}
|
|
66
66
|
});
|
|
67
67
|
|
|
68
|
+
test('handlePlanTask seeds execute-task gate rows for incremental planning', async () => {
|
|
69
|
+
const base = makeTmpBase();
|
|
70
|
+
openDatabase(join(base, '.gsd', 'gsd.db'));
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
seedParent();
|
|
74
|
+
const result = await handlePlanTask(validParams(), base);
|
|
75
|
+
assert.ok(!('error' in result), `unexpected error: ${'error' in result ? result.error : ''}`);
|
|
76
|
+
|
|
77
|
+
const gateIds = getGateResults('M001', 'S02', 'task')
|
|
78
|
+
.filter((gate) => gate.task_id === 'T02')
|
|
79
|
+
.map((gate) => gate.gate_id)
|
|
80
|
+
.sort();
|
|
81
|
+
assert.deepEqual(gateIds, ['Q5', 'Q6', 'Q7']);
|
|
82
|
+
} finally {
|
|
83
|
+
cleanup(base);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
68
87
|
test('handlePlanTask rejects invalid payloads', async () => {
|
|
69
88
|
const base = makeTmpBase();
|
|
70
89
|
openDatabase(join(base, '.gsd', 'gsd.db'));
|
|
@@ -170,6 +189,30 @@ test('handlePlanTask surfaces render failures without changing parse-visible tas
|
|
|
170
189
|
}
|
|
171
190
|
});
|
|
172
191
|
|
|
192
|
+
test('handlePlanTask keeps the sketch flag set when the flat-phase slice plan sync fails (#1083)', async () => {
|
|
193
|
+
const base = makeTmpBase();
|
|
194
|
+
openDatabase(join(base, '.gsd', 'gsd.db'));
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
insertMilestone({ id: 'M001', title: 'Milestone', status: 'active' });
|
|
198
|
+
insertSlice({ id: 'S02', milestoneId: 'M001', title: 'Planning slice', status: 'pending', demo: 'Rendered plans exist.', isSketch: true });
|
|
199
|
+
|
|
200
|
+
// The per-task render writes T02-PLAN.md and succeeds, but the flat-phase
|
|
201
|
+
// slice re-render (the canonical PLAN.md that embeds task checkboxes) targets
|
|
202
|
+
// phases/01-test/01-02-PLAN.md. Pre-creating that path as a directory forces
|
|
203
|
+
// EISDIR on the slice sync, so the canonical slice plan never reflects the
|
|
204
|
+
// task. The sketch flag must therefore stay set — the slice keeps refining —
|
|
205
|
+
// instead of leaving the slice out of refining over a stale plan.
|
|
206
|
+
mkdirSync(join(base, '.gsd', 'phases', '01-test', '01-02-PLAN.md'), { recursive: true });
|
|
207
|
+
|
|
208
|
+
const result = await handlePlanTask(validParams(), base);
|
|
209
|
+
assert.equal(getSlice('M001', 'S02')?.is_sketch, 1, 'sketch flag must stay set when the canonical slice plan could not sync');
|
|
210
|
+
assert.ok(getTask('M001', 'S02', 'T02'), 'the task itself is still persisted so the slice can re-sync on retry');
|
|
211
|
+
} finally {
|
|
212
|
+
cleanup(base);
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
|
|
173
216
|
test('handlePlanTask reruns idempotently and refreshes parse-visible state', async () => {
|
|
174
217
|
const base = makeTmpBase();
|
|
175
218
|
openDatabase(join(base, '.gsd', 'gsd.db'));
|
|
@@ -10,7 +10,7 @@ import * as os from 'node:os';
|
|
|
10
10
|
import { openDatabase, closeDatabase } from '../gsd-db.ts';
|
|
11
11
|
import { handleCompleteTask } from '../tools/complete-task.ts';
|
|
12
12
|
import { readEvents } from '../workflow-events.ts';
|
|
13
|
-
import { readManifest } from '../workflow-manifest.ts';
|
|
13
|
+
import { flushManifest, readManifest } from '../workflow-manifest.ts';
|
|
14
14
|
|
|
15
15
|
function tempDir(): string {
|
|
16
16
|
return fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-post-hook-'));
|
|
@@ -115,8 +115,9 @@ test('post-mutation-hook: state-manifest.json exists after handleCompleteTask',
|
|
|
115
115
|
const result = await handleCompleteTask(makeCompleteTaskParams(), base);
|
|
116
116
|
assert.ok(!('error' in result), `handler should succeed, got: ${JSON.stringify(result)}`);
|
|
117
117
|
|
|
118
|
+
await flushManifest(base);
|
|
118
119
|
const manifestPath = path.join(base, '.gsd', 'state-manifest.json');
|
|
119
|
-
assert.ok(fs.existsSync(manifestPath), 'state-manifest.json should exist after
|
|
120
|
+
assert.ok(fs.existsSync(manifestPath), 'state-manifest.json should exist after manifest flush');
|
|
120
121
|
} finally {
|
|
121
122
|
closeDatabase();
|
|
122
123
|
cleanupDir(base);
|
|
@@ -131,6 +132,7 @@ test('post-mutation-hook: manifest has version 1 and includes completed task', a
|
|
|
131
132
|
|
|
132
133
|
try {
|
|
133
134
|
await handleCompleteTask(makeCompleteTaskParams(), base);
|
|
135
|
+
await flushManifest(base);
|
|
134
136
|
|
|
135
137
|
const manifest = readManifest(base);
|
|
136
138
|
assert.ok(manifest !== null, 'manifest should be readable');
|
|
@@ -72,6 +72,25 @@ function setupDependencyFixture(
|
|
|
72
72
|
mkdirSync(targetSliceDir, { recursive: true });
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
function setupFlatPhaseSlicePlan(base: string): void {
|
|
76
|
+
const phaseDir = join(base, ".gsd", "phases", "01-flat-phase");
|
|
77
|
+
mkdirSync(phaseDir, { recursive: true });
|
|
78
|
+
writeFileSync(
|
|
79
|
+
join(phaseDir, "01-01-PLAN.md"),
|
|
80
|
+
[
|
|
81
|
+
"# S01: Flat phase slice",
|
|
82
|
+
"",
|
|
83
|
+
"## Tasks",
|
|
84
|
+
"",
|
|
85
|
+
"- [ ] **T01: Flat task** `est:15m`",
|
|
86
|
+
"",
|
|
87
|
+
"## Verification",
|
|
88
|
+
"",
|
|
89
|
+
"- Rendered prompt points at this slice plan.",
|
|
90
|
+
].join("\n"),
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
75
94
|
// ─── inlineDependencySummaries truncation ─────────────────────────────────────
|
|
76
95
|
|
|
77
96
|
describe("prompt-budget: inlineDependencySummaries truncation", () => {
|
|
@@ -412,6 +431,26 @@ describe("prompt-budget: execute-task template", () => {
|
|
|
412
431
|
}
|
|
413
432
|
});
|
|
414
433
|
|
|
434
|
+
it("flat-phase execute-task fallback points at the slice plan", async () => {
|
|
435
|
+
const base = createFixtureBase();
|
|
436
|
+
try {
|
|
437
|
+
setupFlatPhaseSlicePlan(base);
|
|
438
|
+
|
|
439
|
+
const prompt = await buildExecuteTaskPrompt("M001", "S01", "Slice", "T01", "Task", base, {
|
|
440
|
+
level: "minimal",
|
|
441
|
+
sessionContextWindow: 128_000,
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
assert.match(
|
|
445
|
+
prompt,
|
|
446
|
+
/Task plan not found at dispatch time\. Read `\.gsd\/phases\/01-flat-phase\/01-01-PLAN\.md` before executing\./,
|
|
447
|
+
);
|
|
448
|
+
assert.doesNotMatch(prompt, /\.gsd\/phases\/01-flat-phase\/tasks\/T01-PLAN\.md/);
|
|
449
|
+
} finally {
|
|
450
|
+
cleanup(base);
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
|
|
415
454
|
it("verificationBudget format varies with context window size", () => {
|
|
416
455
|
const budget128K = computeBudgets(128_000);
|
|
417
456
|
const budget1M = computeBudgets(1_000_000);
|
|
@@ -566,6 +605,25 @@ describe("prompt-budget: execute-task builder truncation pattern", () => {
|
|
|
566
605
|
});
|
|
567
606
|
|
|
568
607
|
describe("prompt-budget: reactive-execute builder", () => {
|
|
608
|
+
it("flat-phase subagent fallback points at the slice plan", async () => {
|
|
609
|
+
const base = createFixtureBase();
|
|
610
|
+
try {
|
|
611
|
+
setupFlatPhaseSlicePlan(base);
|
|
612
|
+
|
|
613
|
+
const prompt = await buildReactiveExecutePrompt("M001", "Milestone", "S01", "Slice", ["T01"], base, undefined, {
|
|
614
|
+
sessionContextWindow: 128_000,
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
assert.match(
|
|
618
|
+
prompt,
|
|
619
|
+
/Task plan not found at dispatch time\. Read `\.gsd\/phases\/01-flat-phase\/01-01-PLAN\.md` before executing\./,
|
|
620
|
+
);
|
|
621
|
+
assert.doesNotMatch(prompt, /\.gsd\/phases\/01-flat-phase\/tasks\/T01-PLAN\.md/);
|
|
622
|
+
} finally {
|
|
623
|
+
cleanup(base);
|
|
624
|
+
}
|
|
625
|
+
});
|
|
626
|
+
|
|
569
627
|
it("caps dependency carry-forward per ready subagent", async () => {
|
|
570
628
|
const base = createFixtureBase();
|
|
571
629
|
try {
|
|
@@ -533,6 +533,7 @@ test("plan-milestone prompt references DB-backed planning tool and explicitly fo
|
|
|
533
533
|
assert.match(prompt, /Do \*\*not\*\* write `?\{\{outputPath\}\}`?, `?ROADMAP\.md`?, or other planning artifacts manually/i);
|
|
534
534
|
assert.match(prompt, /NEVER call `gsd_plan_milestone` with only `milestoneId` and `sliceId`/);
|
|
535
535
|
assert.match(prompt, /gsd_plan_slice/);
|
|
536
|
+
assert.match(prompt, /gsd_plan_task/);
|
|
536
537
|
});
|
|
537
538
|
|
|
538
539
|
test("discuss prompts forbid gsd_plan_milestone slice-only tool args", () => {
|
|
@@ -548,12 +549,12 @@ test("plan-slice prompt no longer frames direct PLAN writes as the source of tru
|
|
|
548
549
|
assert.match(prompt, /Do \*\*not\*\* rely on direct `PLAN\.md` writes as the source of truth/i);
|
|
549
550
|
});
|
|
550
551
|
|
|
551
|
-
test("plan-slice prompt explicitly names
|
|
552
|
+
test("plan-slice prompt explicitly names incremental DB-backed planning tools", () => {
|
|
552
553
|
const prompt = readPrompt("plan-slice");
|
|
553
554
|
assert.match(prompt, /gsd_plan_slice/);
|
|
554
555
|
assert.match(prompt, /gsd_plan_task/);
|
|
555
|
-
|
|
556
|
-
assert.match(prompt, /DB-backed
|
|
556
|
+
assert.match(prompt, /Persist incrementally through DB-backed tools/i);
|
|
557
|
+
assert.match(prompt, /DB-backed tools are the canonical write path/i);
|
|
557
558
|
});
|
|
558
559
|
|
|
559
560
|
test("plan-slice prompt does not instruct direct file writes as a primary step", () => {
|
|
@@ -562,11 +563,12 @@ test("plan-slice prompt does not instruct direct file writes as a primary step",
|
|
|
562
563
|
assert.doesNotMatch(prompt, /^\d+\.\s+Write `?\{\{outputPath\}\}`?\s*$/m);
|
|
563
564
|
});
|
|
564
565
|
|
|
565
|
-
test("plan-slice prompt
|
|
566
|
+
test("plan-slice prompt requires per-task incremental persistence", () => {
|
|
566
567
|
const prompt = readPrompt("plan-slice");
|
|
567
|
-
// gsd_plan_slice persists tasks in its transaction — no separate gsd_plan_task calls needed
|
|
568
568
|
assert.match(prompt, /gsd_plan_task/);
|
|
569
|
-
assert.match(prompt, /
|
|
569
|
+
assert.match(prompt, /omit `tasks` for this metadata call/i);
|
|
570
|
+
assert.match(prompt, /call `gsd_plan_task` once per task/i);
|
|
571
|
+
assert.doesNotMatch(prompt, /do not call `gsd_plan_task`/i);
|
|
570
572
|
});
|
|
571
573
|
|
|
572
574
|
test("plan-slice prompt references web app UAT guidance when planning browser work", () => {
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
|
|
4
|
+
import { autoSession } from "../auto-runtime-state.ts";
|
|
5
|
+
import { registerHooks } from "../bootstrap/register-hooks.ts";
|
|
6
|
+
import { resetToolCallLoopGuard } from "../bootstrap/tool-call-loop-guard.ts";
|
|
7
|
+
|
|
8
|
+
type Handler = (event: any, ctx?: any) => Promise<any> | any;
|
|
9
|
+
|
|
10
|
+
function makeHookHarness(): {
|
|
11
|
+
emitToolCall: (toolName: string, input: Record<string, unknown>) => Promise<any>;
|
|
12
|
+
} {
|
|
13
|
+
const handlers = new Map<string, Handler[]>();
|
|
14
|
+
const pi = {
|
|
15
|
+
on(event: string, handler: Handler) {
|
|
16
|
+
const existing = handlers.get(event) ?? [];
|
|
17
|
+
existing.push(handler);
|
|
18
|
+
handlers.set(event, existing);
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
const ctx = {
|
|
22
|
+
cwd: process.cwd(),
|
|
23
|
+
ui: { notify: () => undefined },
|
|
24
|
+
};
|
|
25
|
+
let callId = 0;
|
|
26
|
+
|
|
27
|
+
registerHooks(pi as any, []);
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
async emitToolCall(toolName: string, input: Record<string, unknown>): Promise<any> {
|
|
31
|
+
callId += 1;
|
|
32
|
+
const loopGuardHandler = handlers.get("tool_call")?.[0];
|
|
33
|
+
assert.ok(loopGuardHandler, "loop-guard tool_call handler should be registered");
|
|
34
|
+
return loopGuardHandler({ toolCallId: `loop-${callId}`, toolName, input }, ctx);
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
test("register-hooks keeps loop-guard block reason interactive outside auto-mode", async (t) => {
|
|
40
|
+
autoSession.reset();
|
|
41
|
+
resetToolCallLoopGuard();
|
|
42
|
+
t.after(() => {
|
|
43
|
+
autoSession.reset();
|
|
44
|
+
resetToolCallLoopGuard();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const { emitToolCall } = makeHookHarness();
|
|
48
|
+
|
|
49
|
+
for (let i = 0; i < 4; i += 1) {
|
|
50
|
+
await emitToolCall("web_search", { query: "same query" });
|
|
51
|
+
}
|
|
52
|
+
const block = await emitToolCall("web_search", { query: "same query" });
|
|
53
|
+
|
|
54
|
+
assert.equal(block?.block, true);
|
|
55
|
+
assert.match(block.reason, /respond to the user in text/);
|
|
56
|
+
assert.doesNotMatch(block.reason, /auto-mode recovery\/replan path/);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("register-hooks rewrites loop-guard block reason for auto-mode identical-args blocks", async (t) => {
|
|
60
|
+
autoSession.reset();
|
|
61
|
+
resetToolCallLoopGuard();
|
|
62
|
+
autoSession.active = true;
|
|
63
|
+
t.after(() => {
|
|
64
|
+
autoSession.reset();
|
|
65
|
+
resetToolCallLoopGuard();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const { emitToolCall } = makeHookHarness();
|
|
69
|
+
|
|
70
|
+
for (let i = 0; i < 4; i += 1) {
|
|
71
|
+
await emitToolCall("web_search", { query: "same query" });
|
|
72
|
+
}
|
|
73
|
+
const block = await emitToolCall("web_search", { query: "same query" });
|
|
74
|
+
|
|
75
|
+
assert.equal(block?.block, true);
|
|
76
|
+
assert.match(block.reason, /Tool loop detected \(identical args\): web_search/);
|
|
77
|
+
assert.match(block.reason, /Do not re-issue this blocked tool/);
|
|
78
|
+
assert.match(block.reason, /auto-mode recovery\/replan path/);
|
|
79
|
+
assert.doesNotMatch(block.reason, /respond to the user in text/);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("register-hooks rewrites loop-guard block reason for auto-mode per-tool blocks", async (t) => {
|
|
83
|
+
autoSession.reset();
|
|
84
|
+
resetToolCallLoopGuard();
|
|
85
|
+
autoSession.active = true;
|
|
86
|
+
t.after(() => {
|
|
87
|
+
autoSession.reset();
|
|
88
|
+
resetToolCallLoopGuard();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const { emitToolCall } = makeHookHarness();
|
|
92
|
+
|
|
93
|
+
for (let i = 0; i < 6; i += 1) {
|
|
94
|
+
await emitToolCall("gsd_complete_milestone", { milestone: `M${i}` });
|
|
95
|
+
}
|
|
96
|
+
const block = await emitToolCall("gsd_complete_milestone", { milestone: "M7" });
|
|
97
|
+
|
|
98
|
+
assert.equal(block?.block, true);
|
|
99
|
+
assert.match(block.reason, /Tool loop detected \(repeated tool\): gsd_complete_milestone/);
|
|
100
|
+
assert.match(block.reason, /Do not re-issue this blocked tool/);
|
|
101
|
+
assert.match(block.reason, /auto-mode recovery\/replan path/);
|
|
102
|
+
assert.doesNotMatch(block.reason, /respond to the user in text/);
|
|
103
|
+
});
|
|
@@ -7,7 +7,26 @@ import { closeSync, ftruncateSync, openSync, statSync, writeFileSync } from "nod
|
|
|
7
7
|
import { join } from "node:path";
|
|
8
8
|
|
|
9
9
|
import { captureRootDirtySnapshot } from "../root-write-leak-guard.ts";
|
|
10
|
-
import { cleanup, git, makeTempRepo } from "./test-utils.ts";
|
|
10
|
+
import { cleanup, createFile, git, makeTempRepo } from "./test-utils.ts";
|
|
11
|
+
|
|
12
|
+
test("captureRootDirtySnapshot ignores unignored GSD root runtime directories", () => {
|
|
13
|
+
const base = makeTempRepo("gsd-root-dirty-runtime-");
|
|
14
|
+
try {
|
|
15
|
+
createFile(base, ".gsd/STATE.md", "state\n");
|
|
16
|
+
createFile(base, ".gsd-backups/snap-001", "backup\n");
|
|
17
|
+
createFile(base, ".gsd-worktrees/M001/file.txt", "worktree\n");
|
|
18
|
+
createFile(base, "src/foo.ts", "export {};\n");
|
|
19
|
+
|
|
20
|
+
const snapshot = captureRootDirtySnapshot(base);
|
|
21
|
+
|
|
22
|
+
assert.equal(snapshot.has(".gsd/STATE.md"), false);
|
|
23
|
+
assert.equal(snapshot.has(".gsd-backups/snap-001"), false);
|
|
24
|
+
assert.equal(snapshot.has(".gsd-worktrees/M001/file.txt"), false);
|
|
25
|
+
assert.equal(snapshot.has("src/foo.ts"), true);
|
|
26
|
+
} finally {
|
|
27
|
+
cleanup(base);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
11
30
|
|
|
12
31
|
test("captureRootDirtySnapshot does not read dirty files larger than Node's Buffer limit", () => {
|
|
13
32
|
const base = makeTempRepo("gsd-root-dirty-large-");
|
|
@@ -184,10 +184,15 @@ test("auto Unit tool scope allows reactive-execute status and diagnostic tools",
|
|
|
184
184
|
}
|
|
185
185
|
});
|
|
186
186
|
|
|
187
|
-
test("auto Unit tool scope
|
|
188
|
-
|
|
187
|
+
test("auto Unit tool scope allows incremental task planning during plan-slice", () => {
|
|
188
|
+
const result = shouldBlockAutoUnitToolCall("plan-slice", "gsd_plan_task");
|
|
189
|
+
assert.equal(result.block, false, "plan-slice should be able to call gsd_plan_task incrementally");
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test("auto Unit tool scope still blocks per-task planner outside plan-slice", () => {
|
|
193
|
+
for (const unitType of ["refine-slice", "replan-slice"]) {
|
|
189
194
|
const result = shouldBlockAutoUnitToolCall(unitType, "gsd_plan_task");
|
|
190
|
-
assert.equal(result.block, true, `${unitType} should not call
|
|
195
|
+
assert.equal(result.block, true, `${unitType} should not call gsd_plan_task`);
|
|
191
196
|
}
|
|
192
197
|
});
|
|
193
198
|
|
|
@@ -7,6 +7,7 @@ import { test } from 'node:test';
|
|
|
7
7
|
import assert from 'node:assert/strict';
|
|
8
8
|
import {
|
|
9
9
|
checkToolCallLoop,
|
|
10
|
+
recordToolCallLoopMutation,
|
|
10
11
|
resetToolCallLoopGuard,
|
|
11
12
|
disableToolCallLoopGuard,
|
|
12
13
|
getToolCallLoopCount,
|
|
@@ -223,6 +224,62 @@ console.log('\n── Loop guard: repeatable tools get the higher cap (#783) ─
|
|
|
223
224
|
assert.ok(blocked.reason!.includes('cap 15'), 'reason should mention the repeatable cap');
|
|
224
225
|
}
|
|
225
226
|
|
|
227
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
228
|
+
// Mutation progress decays per-tool counts (#1092)
|
|
229
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
230
|
+
|
|
231
|
+
console.log('\n── Loop guard: mutation progress decays per-tool counts (#1092) ──');
|
|
232
|
+
|
|
233
|
+
{
|
|
234
|
+
resetToolCallLoopGuard();
|
|
235
|
+
|
|
236
|
+
// bash is called more than the repeatable cap, but each count is separated
|
|
237
|
+
// by a successful file mutation. Guard 2 should treat that as progress and
|
|
238
|
+
// avoid blocking the progressing unit.
|
|
239
|
+
for (let i = 1; i <= 20; i++) {
|
|
240
|
+
const result = checkToolCallLoop('bash', { command: `echo ${i}` });
|
|
241
|
+
assert.ok(result.block === false, `bash call ${i} after mutation progress should be allowed`);
|
|
242
|
+
assert.deepStrictEqual(getToolCallCountForTool('bash'), 1, `bash count should decay before call ${i}`);
|
|
243
|
+
|
|
244
|
+
const mutationTool = i % 2 === 0 ? 'write' : 'edit';
|
|
245
|
+
const mutation = checkToolCallLoop(mutationTool, {
|
|
246
|
+
path: `file-${i}.ts`,
|
|
247
|
+
content: `content ${i}`,
|
|
248
|
+
});
|
|
249
|
+
assert.ok(mutation.block === false, `${mutationTool} mutation ${i} should be allowed`);
|
|
250
|
+
recordToolCallLoopMutation(mutationTool);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
255
|
+
// Distinct read-only navigation calls are context gathering, not repeated-tool loops.
|
|
256
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
257
|
+
|
|
258
|
+
console.log('\n── Loop guard: distinct read-only navigation calls are not capped by per-tool count ──');
|
|
259
|
+
|
|
260
|
+
{
|
|
261
|
+
const exemptTools = ['find', 'glob', 'grep', 'ls', 'read', 'search_and_read'];
|
|
262
|
+
|
|
263
|
+
for (const toolName of exemptTools) {
|
|
264
|
+
resetToolCallLoopGuard();
|
|
265
|
+
|
|
266
|
+
for (let i = 1; i <= 20; i++) {
|
|
267
|
+
const result = checkToolCallLoop(toolName, { path: `file-${i}.ts` });
|
|
268
|
+
assert.ok(result.block === false, `distinct ${toolName} call ${i} should be allowed`);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
resetToolCallLoopGuard();
|
|
272
|
+
for (let i = 1; i <= 4; i++) {
|
|
273
|
+
const result = checkToolCallLoop(toolName, { path: 'same-file.ts' });
|
|
274
|
+
assert.ok(result.block === false, `identical ${toolName} call ${i} should be allowed`);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const blocked = checkToolCallLoop(toolName, { path: 'same-file.ts' });
|
|
278
|
+
assert.ok(blocked.block === true, `5th identical ${toolName} call should still be blocked`);
|
|
279
|
+
assert.ok(blocked.reason?.includes('identical args'), `${toolName} loops should be caught by Guard 1`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
226
283
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
227
284
|
// Per-tool counts are independent per tool and reset together
|
|
228
285
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -245,15 +302,15 @@ console.log('\n── Loop guard: per-tool counts are independent and reset toge
|
|
|
245
302
|
}
|
|
246
303
|
|
|
247
304
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
248
|
-
//
|
|
249
|
-
//
|
|
250
|
-
//
|
|
305
|
+
// Non-exempt repeatable tools from core-session-tools.ts get the higher cap.
|
|
306
|
+
// Read-only navigation tools are exempted earlier; mutating/execution repeatable
|
|
307
|
+
// tools (e.g. gsd_exec) must still trip the per-tool cap.
|
|
251
308
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
252
309
|
|
|
253
|
-
console.log('\n── Loop guard:
|
|
310
|
+
console.log('\n── Loop guard: non-exempt repeatable tools get high cap ──');
|
|
254
311
|
|
|
255
312
|
{
|
|
256
|
-
for (const toolName of ['bg_shell', '
|
|
313
|
+
for (const toolName of ['bg_shell', 'gsd_exec', 'write']) {
|
|
257
314
|
resetToolCallLoopGuard();
|
|
258
315
|
|
|
259
316
|
// Should allow up to 15 varied calls without blocking.
|
|
@@ -385,14 +385,17 @@ test("gsd_plan_slice — enrichment fields are optional", () => {
|
|
|
385
385
|
|
|
386
386
|
const required = new Set(getRequiredProps(tool));
|
|
387
387
|
|
|
388
|
-
// Core fields
|
|
389
|
-
|
|
388
|
+
// Core fields. `tasks` is intentionally NOT here: incremental planning
|
|
389
|
+
// (#1027) lets gsd_plan_slice persist slice metadata only, then add tasks
|
|
390
|
+
// one at a time via gsd_plan_task, so tasks must be optional.
|
|
391
|
+
const coreRequired = ["milestoneId", "sliceId", "goal"];
|
|
390
392
|
for (const field of coreRequired) {
|
|
391
393
|
assert.ok(required.has(field), `core field "${field}" must be required`);
|
|
392
394
|
}
|
|
393
395
|
|
|
394
|
-
// Enrichment fields
|
|
396
|
+
// Enrichment fields (plus tasks, optional for incremental planning).
|
|
395
397
|
const enrichmentFields = [
|
|
398
|
+
"tasks",
|
|
396
399
|
"successCriteria",
|
|
397
400
|
"proofLevel",
|
|
398
401
|
"integrationClosure",
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
handleUndo,
|
|
11
11
|
handleUndoTask,
|
|
12
12
|
handleResetSlice,
|
|
13
|
+
parseActivityLogFilename,
|
|
13
14
|
uncheckTaskInPlan,
|
|
14
15
|
} from "../undo.ts";
|
|
15
16
|
import {
|
|
@@ -67,6 +68,37 @@ test("handleUndo without --force only warns and leaves completed units intact",
|
|
|
67
68
|
}
|
|
68
69
|
});
|
|
69
70
|
|
|
71
|
+
test("handleUndo execute-task with --force reopens the task row and re-renders plan", async () => {
|
|
72
|
+
const base = makeTempDir("gsd-undo-execute-task");
|
|
73
|
+
try {
|
|
74
|
+
setupTaskFixture(base);
|
|
75
|
+
mkdirSync(join(base, ".gsd", "activity"), { recursive: true });
|
|
76
|
+
writeFileSync(
|
|
77
|
+
join(base, ".gsd", "activity", "001-execute-task-M001-S01-T01.jsonl"),
|
|
78
|
+
"",
|
|
79
|
+
"utf-8",
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const { notifications, ctx } = makeCtx();
|
|
83
|
+
await handleUndo("--force", ctx, {} as any, base);
|
|
84
|
+
|
|
85
|
+
const task = getTask("M001", "S01", "T01");
|
|
86
|
+
assert.equal(task?.status, "pending");
|
|
87
|
+
|
|
88
|
+
const planContent = readFileSync(
|
|
89
|
+
join(base, ".gsd", "phases", "01-test", "01-01-PLAN.md"),
|
|
90
|
+
"utf-8",
|
|
91
|
+
);
|
|
92
|
+
assert.match(planContent, /\[ \] \*\*T01\*\*:/);
|
|
93
|
+
|
|
94
|
+
assert.equal(notifications[0]?.level, "success");
|
|
95
|
+
assert.match(notifications[0]?.message ?? "", /Unchecked task in PLAN/);
|
|
96
|
+
} finally {
|
|
97
|
+
closeDatabase();
|
|
98
|
+
rmSync(base, { recursive: true, force: true });
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
70
102
|
test("uncheckTaskInPlan flips a checked task back to unchecked", () => {
|
|
71
103
|
const base = makeTempDir("gsd-undo-plan");
|
|
72
104
|
try {
|
|
@@ -154,6 +186,48 @@ test("extractCommitShas ignores malformed commit tokens", () => {
|
|
|
154
186
|
assert.deepEqual(extractCommitShas(content), ["1234567"]);
|
|
155
187
|
});
|
|
156
188
|
|
|
189
|
+
test("parseActivityLogFilename splits hyphenated unit types from their IDs", () => {
|
|
190
|
+
// Task-level: unit type and ID both contain hyphens.
|
|
191
|
+
assert.deepEqual(parseActivityLogFilename("001-execute-task-M001-S01-T01.jsonl"), {
|
|
192
|
+
unitType: "execute-task",
|
|
193
|
+
unitId: "M001-S01-T01",
|
|
194
|
+
});
|
|
195
|
+
// Variant must win over its shorter prefix ("execute-task").
|
|
196
|
+
assert.deepEqual(parseActivityLogFilename("002-execute-task-simple-M001-S02-T03.jsonl"), {
|
|
197
|
+
unitType: "execute-task-simple",
|
|
198
|
+
unitId: "M001-S02-T03",
|
|
199
|
+
});
|
|
200
|
+
// Milestone- and slice-shaped IDs.
|
|
201
|
+
assert.deepEqual(parseActivityLogFilename("003-research-milestone-M001.jsonl"), {
|
|
202
|
+
unitType: "research-milestone",
|
|
203
|
+
unitId: "M001",
|
|
204
|
+
});
|
|
205
|
+
assert.deepEqual(parseActivityLogFilename("004-plan-slice-M001-S01.jsonl"), {
|
|
206
|
+
unitType: "plan-slice",
|
|
207
|
+
unitId: "M001-S01",
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test("parseActivityLogFilename accepts non-milestone-shaped unit IDs", () => {
|
|
212
|
+
// Regression (#1057): project-level units use IDs that do not start with
|
|
213
|
+
// "M<digit>". A milestone-shaped regex made /gsd undo bail out with
|
|
214
|
+
// "could not parse latest activity log" when one of these was most recent.
|
|
215
|
+
assert.deepEqual(parseActivityLogFilename("005-discuss-project-PROJECT.jsonl"), {
|
|
216
|
+
unitType: "discuss-project",
|
|
217
|
+
unitId: "PROJECT",
|
|
218
|
+
});
|
|
219
|
+
assert.deepEqual(parseActivityLogFilename("006-workflow-preferences-WORKFLOW-PREFS.jsonl"), {
|
|
220
|
+
unitType: "workflow-preferences",
|
|
221
|
+
unitId: "WORKFLOW-PREFS",
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test("parseActivityLogFilename returns null for unrecognized names", () => {
|
|
226
|
+
assert.equal(parseActivityLogFilename("007-not-a-real-unit-M001.jsonl"), null);
|
|
227
|
+
assert.equal(parseActivityLogFilename("no-sequence-prefix.jsonl"), null);
|
|
228
|
+
assert.equal(parseActivityLogFilename("001-execute-task-M001-S01-T01.txt"), null);
|
|
229
|
+
});
|
|
230
|
+
|
|
157
231
|
// ─── handleUndoTask tests ────────────────────────────────────────────────────
|
|
158
232
|
|
|
159
233
|
function makeCtx(): { notifications: Array<{ message: string; level: string }>; ctx: any } {
|
|
@@ -18,6 +18,7 @@ import { join } from "node:path";
|
|
|
18
18
|
import { execSync } from "node:child_process";
|
|
19
19
|
import { GIT_NO_PROMPT_ENV } from "../git-constants.ts";
|
|
20
20
|
import { handleTurnGitActionError, runTurnGitAction } from "../git-service.ts";
|
|
21
|
+
import { _resetLogs, drainLogs } from "../workflow-logger.ts";
|
|
21
22
|
|
|
22
23
|
function run(cmd: string, cwd: string): string {
|
|
23
24
|
return execSync(cmd, { cwd, stdio: "pipe", encoding: "utf-8" }).trim();
|
|
@@ -111,6 +112,43 @@ workspace:
|
|
|
111
112
|
}
|
|
112
113
|
});
|
|
113
114
|
|
|
115
|
+
test("uok gitops turn action status-only reports dirty undeclared nested git repositories", () => {
|
|
116
|
+
const root = mkdtempSync(join(tmpdir(), "gsd-uok-gitops-nested-"));
|
|
117
|
+
try {
|
|
118
|
+
initRepo(root);
|
|
119
|
+
mkdirSync(join(root, "nested"), { recursive: true });
|
|
120
|
+
initRepo(join(root, "nested"));
|
|
121
|
+
writeFileSync(join(root, ".gitignore"), "nested/\n", "utf-8");
|
|
122
|
+
run("git add .gitignore", root);
|
|
123
|
+
run('git commit -m "chore: ignore nested repo"', root);
|
|
124
|
+
|
|
125
|
+
writeFileSync(join(root, "nested", "README.md"), "# Dirty nested\n", "utf-8");
|
|
126
|
+
|
|
127
|
+
_resetLogs();
|
|
128
|
+
const result = runTurnGitAction({
|
|
129
|
+
basePath: root,
|
|
130
|
+
action: "status-only",
|
|
131
|
+
unitType: "execute-task",
|
|
132
|
+
unitId: "M001/S01/T01",
|
|
133
|
+
});
|
|
134
|
+
const warnings = drainLogs();
|
|
135
|
+
|
|
136
|
+
assert.equal(result.status, "ok");
|
|
137
|
+
assert.equal(result.dirty, true);
|
|
138
|
+
assert.deepEqual(result.dirtyRepositories, {
|
|
139
|
+
project: false,
|
|
140
|
+
"nested (undeclared)": true,
|
|
141
|
+
});
|
|
142
|
+
assert.ok(
|
|
143
|
+
warnings.some((entry) => entry.message.includes("undeclared dirty nested git repo nested")),
|
|
144
|
+
"dirty undeclared nested repo should be logged as a warning",
|
|
145
|
+
);
|
|
146
|
+
} finally {
|
|
147
|
+
_resetLogs();
|
|
148
|
+
rmSync(root, { recursive: true, force: true });
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
114
152
|
test("uok gitops turn action snapshot writes snapshot refs", () => {
|
|
115
153
|
const repo = makeRepo();
|
|
116
154
|
try {
|