@opengsd/gsd-pi 1.9.0-dev.5c1a3ff2 → 1.9.0-dev.97ef11e3
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/dist/headless.js +2 -1
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/gsd/auto/loop.js +24 -4
- package/dist/resources/extensions/gsd/auto/run-unit.js +19 -1
- package/dist/resources/extensions/gsd/auto/unit-phase.js +37 -5
- package/dist/resources/extensions/gsd/auto-dispatch.js +2 -0
- package/dist/resources/extensions/gsd/auto-prompts.js +12 -4
- package/dist/resources/extensions/gsd/auto-runtime-state.js +5 -5
- package/dist/resources/extensions/gsd/auto-start.js +29 -9
- package/dist/resources/extensions/gsd/auto-tool-tracking.js +40 -0
- package/dist/resources/extensions/gsd/auto-worktree-sync.js +7 -3
- package/dist/resources/extensions/gsd/auto.js +15 -5
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +2 -2
- package/dist/resources/extensions/gsd/commands-verdict.js +10 -9
- package/dist/resources/extensions/gsd/compat/compat-marker.js +80 -5
- package/dist/resources/extensions/gsd/db/engine.js +5 -2
- package/dist/resources/extensions/gsd/doctor-engine-checks.js +21 -2
- package/dist/resources/extensions/gsd/error-classifier.js +12 -2
- package/dist/resources/extensions/gsd/exec-sandbox.js +1 -0
- package/dist/resources/extensions/gsd/flat-phase-migration.js +12 -0
- package/dist/resources/extensions/gsd/gsd-db.js +75 -31
- package/dist/resources/extensions/gsd/guided-flow.js +2 -2
- package/dist/resources/extensions/gsd/markdown-renderer.js +3 -9
- package/dist/resources/extensions/gsd/paths.js +17 -7
- package/dist/resources/extensions/gsd/prompts/validate-milestone.md +5 -5
- package/dist/resources/extensions/gsd/state-reconciliation/drift/external-markdown-edit.js +98 -2
- package/dist/resources/extensions/gsd/state-reconciliation/drift/external-planning-edit.js +4 -1
- package/dist/resources/extensions/gsd/state-reconciliation/drift/roadmap.js +5 -2
- package/dist/resources/extensions/gsd/stop-notice.js +9 -0
- package/dist/resources/extensions/gsd/worktree-state-projection.js +20 -18
- package/dist/resources/extensions/shared/interview-ui.js +43 -6
- 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/chunks/1991.js +2 -2
- package/dist/web/standalone/.next/server/chunks/5124.js +1 -1
- 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 +2 -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/package.json +7 -7
- package/packages/gsd-cloud/README.md +9 -3
- package/packages/gsd-cloud/dist/background-cli.test.d.ts +2 -0
- package/packages/gsd-cloud/dist/background-cli.test.d.ts.map +1 -0
- package/packages/gsd-cloud/dist/background-cli.test.js +248 -0
- package/packages/gsd-cloud/dist/background-cli.test.js.map +1 -0
- package/packages/gsd-cloud/dist/cli.d.ts.map +1 -1
- package/packages/gsd-cloud/dist/cli.js +121 -15
- package/packages/gsd-cloud/dist/cli.js.map +1 -1
- package/packages/gsd-cloud/dist/cloud-config.d.ts +1 -1
- package/packages/gsd-cloud/dist/cloud-config.d.ts.map +1 -1
- package/packages/gsd-cloud/dist/cloud-config.js +7 -1
- package/packages/gsd-cloud/dist/cloud-config.js.map +1 -1
- package/packages/gsd-cloud/dist/cloud-runtime.d.ts +1 -3
- package/packages/gsd-cloud/dist/cloud-runtime.d.ts.map +1 -1
- package/packages/gsd-cloud/dist/cloud-runtime.js +9 -7
- package/packages/gsd-cloud/dist/cloud-runtime.js.map +1 -1
- package/packages/gsd-cloud/dist/runtime-process.d.ts +27 -0
- package/packages/gsd-cloud/dist/runtime-process.d.ts.map +1 -0
- package/packages/gsd-cloud/dist/runtime-process.js +272 -0
- package/packages/gsd-cloud/dist/runtime-process.js.map +1 -0
- package/packages/gsd-cloud/dist/runtime-process.test.d.ts +2 -0
- package/packages/gsd-cloud/dist/runtime-process.test.d.ts.map +1 -0
- package/packages/gsd-cloud/dist/runtime-process.test.js +97 -0
- package/packages/gsd-cloud/dist/runtime-process.test.js.map +1 -0
- package/packages/gsd-cloud/package.json +2 -2
- package/packages/mcp-server/dist/cli-runner.d.ts +3 -0
- package/packages/mcp-server/dist/cli-runner.d.ts.map +1 -1
- package/packages/mcp-server/dist/cli-runner.js +119 -33
- package/packages/mcp-server/dist/cli-runner.js.map +1 -1
- package/packages/mcp-server/package.json +4 -4
- package/packages/native/package.json +1 -1
- package/packages/pi-agent-core/package.json +1 -1
- package/packages/pi-ai/dist/models.generated.d.ts +17 -0
- package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
- package/packages/pi-ai/dist/models.generated.js +17 -0
- package/packages/pi-ai/dist/models.generated.js.map +1 -1
- package/packages/pi-ai/dist/providers/openai-completions.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/openai-completions.js +103 -0
- package/packages/pi-ai/dist/providers/openai-completions.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/gsd/auto/loop.ts +48 -4
- package/src/resources/extensions/gsd/auto/run-unit.ts +20 -1
- package/src/resources/extensions/gsd/auto/unit-phase.ts +41 -5
- package/src/resources/extensions/gsd/auto-dispatch.ts +1 -0
- package/src/resources/extensions/gsd/auto-prompts.ts +14 -4
- package/src/resources/extensions/gsd/auto-runtime-state.ts +12 -7
- package/src/resources/extensions/gsd/auto-start.ts +31 -8
- package/src/resources/extensions/gsd/auto-tool-tracking.ts +56 -0
- package/src/resources/extensions/gsd/auto-worktree-sync.ts +10 -3
- package/src/resources/extensions/gsd/auto.ts +20 -7
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +2 -2
- package/src/resources/extensions/gsd/commands-verdict.ts +16 -9
- package/src/resources/extensions/gsd/compat/compat-marker.ts +99 -5
- package/src/resources/extensions/gsd/db/engine.ts +5 -2
- package/src/resources/extensions/gsd/doctor-engine-checks.ts +27 -2
- package/src/resources/extensions/gsd/error-classifier.ts +13 -2
- package/src/resources/extensions/gsd/exec-sandbox.ts +1 -0
- package/src/resources/extensions/gsd/flat-phase-migration.ts +14 -0
- package/src/resources/extensions/gsd/gsd-db.ts +85 -33
- package/src/resources/extensions/gsd/guided-flow.ts +2 -2
- package/src/resources/extensions/gsd/markdown-renderer.ts +3 -9
- package/src/resources/extensions/gsd/paths.ts +17 -7
- package/src/resources/extensions/gsd/prompts/validate-milestone.md +5 -5
- package/src/resources/extensions/gsd/state-reconciliation/drift/external-markdown-edit.ts +132 -2
- package/src/resources/extensions/gsd/state-reconciliation/drift/external-planning-edit.ts +4 -1
- package/src/resources/extensions/gsd/state-reconciliation/drift/roadmap.ts +4 -2
- package/src/resources/extensions/gsd/stop-notice.ts +11 -0
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +264 -0
- package/src/resources/extensions/gsd/tests/auto-start-index-lock.test.ts +21 -1
- package/src/resources/extensions/gsd/tests/auto-wrapup-inflight-guard.test.ts +74 -1
- package/src/resources/extensions/gsd/tests/commands-verdict.test.ts +10 -0
- package/src/resources/extensions/gsd/tests/compat-marker.test.ts +25 -0
- package/src/resources/extensions/gsd/tests/db-engine-cjs-compat.test.ts +12 -0
- package/src/resources/extensions/gsd/tests/doctor-scope-db-unavailable.test.ts +83 -0
- package/src/resources/extensions/gsd/tests/exec-sandbox.test.ts +33 -1
- package/src/resources/extensions/gsd/tests/external-markdown-edit.test.ts +38 -1
- package/src/resources/extensions/gsd/tests/flat-phase-migration.test.ts +8 -9
- package/src/resources/extensions/gsd/tests/flat-phase-validation-path.test.ts +20 -1
- package/src/resources/extensions/gsd/tests/gsd-db.test.ts +32 -0
- package/src/resources/extensions/gsd/tests/guided-discuss-milestone-prompt-rendering.test.ts +22 -0
- package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +13 -0
- package/src/resources/extensions/gsd/tests/prompt-budget-enforcement.test.ts +21 -0
- package/src/resources/extensions/gsd/tests/provider-errors.test.ts +121 -1
- package/src/resources/extensions/gsd/tests/register-hooks-loop-guard-auto.test.ts +105 -0
- package/src/resources/extensions/gsd/tests/state-reconciliation-drift.test.ts +52 -0
- package/src/resources/extensions/gsd/tests/stop-notice.test.ts +21 -1
- package/src/resources/extensions/gsd/tests/terminated-transient.test.ts +14 -0
- package/src/resources/extensions/gsd/tests/tool-invocation-error-loop-break.test.ts +19 -0
- package/src/resources/extensions/gsd/tests/validate-milestone-prompt-verification-classes.test.ts +7 -0
- package/src/resources/extensions/gsd/tests/worktree-state-projection.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +60 -0
- package/src/resources/extensions/gsd/worktree-state-projection.ts +21 -24
- package/src/resources/extensions/shared/interview-ui.ts +47 -8
- package/src/resources/extensions/shared/tests/interview-ui-border.test.ts +29 -0
- /package/dist/web/standalone/.next/static/{6YWV7VAPg-jqjfPswrKZb → DXD9fZPnqtjZSlT5xdC3v}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{6YWV7VAPg-jqjfPswrKZb → DXD9fZPnqtjZSlT5xdC3v}/_ssgManifest.js +0 -0
|
@@ -9,13 +9,15 @@
|
|
|
9
9
|
// refreshes the marker entry, closing the loop.
|
|
10
10
|
|
|
11
11
|
import { existsSync, readFileSync } from "node:fs";
|
|
12
|
-
import { join } from "node:path";
|
|
12
|
+
import { basename, join } from "node:path";
|
|
13
13
|
|
|
14
14
|
import {
|
|
15
15
|
computeProjectionSha,
|
|
16
16
|
readCompatMarker,
|
|
17
17
|
writeCompatMarker,
|
|
18
|
+
type ProjectionEntry,
|
|
18
19
|
} from "../../compat/compat-marker.js";
|
|
20
|
+
import { _getAdapter } from "../../gsd-db.js";
|
|
19
21
|
import { logWarning } from "../../workflow-logger.js";
|
|
20
22
|
import type { GSDState } from "../../types.js";
|
|
21
23
|
import type { DriftContext, DriftHandler, DriftRecord } from "../types.js";
|
|
@@ -25,6 +27,102 @@ type ExternalMarkdownEditDrift = Extract<
|
|
|
25
27
|
{ kind: "external-markdown-edit" }
|
|
26
28
|
>;
|
|
27
29
|
|
|
30
|
+
type ArtifactContentRow = {
|
|
31
|
+
full_content: string;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
type ProjectionScope = {
|
|
35
|
+
milestoneId: string;
|
|
36
|
+
sliceId: string | null;
|
|
37
|
+
taskId: string | null;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
function deepestProjectionScope(entities: readonly string[]): ProjectionScope | null {
|
|
41
|
+
let best: string[] | null = null;
|
|
42
|
+
for (const entity of entities) {
|
|
43
|
+
const parts = entity.split("/").filter(Boolean);
|
|
44
|
+
if (parts.length === 0) continue;
|
|
45
|
+
if (!best || parts.length > best.length) best = parts;
|
|
46
|
+
}
|
|
47
|
+
if (!best) return null;
|
|
48
|
+
return {
|
|
49
|
+
milestoneId: best[0]!,
|
|
50
|
+
sliceId: best[1] ?? null,
|
|
51
|
+
taskId: best[2] ?? null,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function artifactTypeFromProjectionPath(projectionPath: string): string | null {
|
|
56
|
+
const name = basename(projectionPath);
|
|
57
|
+
const match = name.match(/^(?:[A-Za-z]\d+|\d+(?:-\d+)?)-(.+)\.md$/);
|
|
58
|
+
return match?.[1]?.toUpperCase() ?? null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function dbProjectionMatches(
|
|
62
|
+
projectionPath: string,
|
|
63
|
+
entry: ProjectionEntry,
|
|
64
|
+
actualSha: string,
|
|
65
|
+
): boolean {
|
|
66
|
+
const adapter = _getAdapter();
|
|
67
|
+
if (!adapter) return false;
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const rows: ArtifactContentRow[] = adapter
|
|
71
|
+
.prepare("SELECT full_content FROM artifacts WHERE path = :path")
|
|
72
|
+
.all({ ":path": projectionPath }) as ArtifactContentRow[];
|
|
73
|
+
|
|
74
|
+
const scope = deepestProjectionScope(entry.entities);
|
|
75
|
+
const artifactType = artifactTypeFromProjectionPath(projectionPath);
|
|
76
|
+
if (scope?.taskId) {
|
|
77
|
+
rows.push(
|
|
78
|
+
...(adapter
|
|
79
|
+
.prepare(
|
|
80
|
+
`SELECT full_content
|
|
81
|
+
FROM artifacts
|
|
82
|
+
WHERE milestone_id = :mid AND slice_id = :sid AND task_id = :tid
|
|
83
|
+
AND (:type IS NULL OR artifact_type = :type)`,
|
|
84
|
+
)
|
|
85
|
+
.all({
|
|
86
|
+
":mid": scope.milestoneId,
|
|
87
|
+
":sid": scope.sliceId,
|
|
88
|
+
":tid": scope.taskId,
|
|
89
|
+
":type": artifactType,
|
|
90
|
+
}) as ArtifactContentRow[]),
|
|
91
|
+
);
|
|
92
|
+
} else if (scope?.sliceId) {
|
|
93
|
+
rows.push(
|
|
94
|
+
...(adapter
|
|
95
|
+
.prepare(
|
|
96
|
+
`SELECT full_content
|
|
97
|
+
FROM artifacts
|
|
98
|
+
WHERE milestone_id = :mid AND slice_id = :sid AND task_id IS NULL
|
|
99
|
+
AND (:type IS NULL OR artifact_type = :type)`,
|
|
100
|
+
)
|
|
101
|
+
.all({
|
|
102
|
+
":mid": scope.milestoneId,
|
|
103
|
+
":sid": scope.sliceId,
|
|
104
|
+
":type": artifactType,
|
|
105
|
+
}) as ArtifactContentRow[]),
|
|
106
|
+
);
|
|
107
|
+
} else if (scope) {
|
|
108
|
+
rows.push(
|
|
109
|
+
...(adapter
|
|
110
|
+
.prepare(
|
|
111
|
+
`SELECT full_content
|
|
112
|
+
FROM artifacts
|
|
113
|
+
WHERE milestone_id = :mid AND slice_id IS NULL AND task_id IS NULL
|
|
114
|
+
AND (:type IS NULL OR artifact_type = :type)`,
|
|
115
|
+
)
|
|
116
|
+
.all({ ":mid": scope.milestoneId, ":type": artifactType }) as ArtifactContentRow[]),
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return rows.some((row) => computeProjectionSha(row.full_content) === actualSha);
|
|
121
|
+
} catch {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
28
126
|
/**
|
|
29
127
|
* Detect sha drift between the marker baseline and current file contents.
|
|
30
128
|
*
|
|
@@ -38,16 +136,43 @@ function detectExternalMarkdownEdit(
|
|
|
38
136
|
_state: GSDState,
|
|
39
137
|
ctx: DriftContext,
|
|
40
138
|
): ExternalMarkdownEditDrift[] {
|
|
41
|
-
const marker = readCompatMarker(ctx.basePath
|
|
139
|
+
const marker = readCompatMarker(ctx.basePath, {
|
|
140
|
+
healInvalidKeys: !ctx.dryRun,
|
|
141
|
+
quarantineInvalid: !ctx.dryRun,
|
|
142
|
+
});
|
|
42
143
|
const entries = Object.entries(marker.projections);
|
|
43
144
|
if (entries.length === 0) return [];
|
|
44
145
|
|
|
45
146
|
const records: ExternalMarkdownEditDrift[] = [];
|
|
147
|
+
let markerChanged = false;
|
|
46
148
|
for (const [projectionPath, entry] of entries) {
|
|
47
149
|
const abs = join(ctx.basePath, ".gsd", projectionPath);
|
|
48
150
|
if (!existsSync(abs)) continue;
|
|
49
151
|
const actual = computeProjectionSha(readFileSync(abs, "utf-8"));
|
|
50
152
|
if (actual === entry.sha) continue;
|
|
153
|
+
if (dbProjectionMatches(projectionPath, entry, actual)) {
|
|
154
|
+
if (!ctx.dryRun) {
|
|
155
|
+
marker.projections[projectionPath] = {
|
|
156
|
+
sha: actual,
|
|
157
|
+
entities: entry.entities,
|
|
158
|
+
};
|
|
159
|
+
markerChanged = true;
|
|
160
|
+
logWarning(
|
|
161
|
+
"reconcile",
|
|
162
|
+
`external-markdown-edit: refreshed stale compat marker for ${projectionPath} (expectedSha=${entry.sha}, actualSha=${actual})`,
|
|
163
|
+
);
|
|
164
|
+
} else {
|
|
165
|
+
logWarning(
|
|
166
|
+
"reconcile",
|
|
167
|
+
`external-markdown-edit: stale compat marker for ${projectionPath} (expectedSha=${entry.sha}, actualSha=${actual})`,
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
logWarning(
|
|
173
|
+
"reconcile",
|
|
174
|
+
`external-markdown-edit drift for ${projectionPath} (expectedSha=${entry.sha}, actualSha=${actual})`,
|
|
175
|
+
);
|
|
51
176
|
records.push({
|
|
52
177
|
kind: "external-markdown-edit",
|
|
53
178
|
projectionPath,
|
|
@@ -56,6 +181,11 @@ function detectExternalMarkdownEdit(
|
|
|
56
181
|
entities: entry.entities,
|
|
57
182
|
});
|
|
58
183
|
}
|
|
184
|
+
if (markerChanged) {
|
|
185
|
+
marker.lastWriter = "gsd-pi";
|
|
186
|
+
marker.lastProjectedAt = new Date().toISOString();
|
|
187
|
+
writeCompatMarker(ctx.basePath, marker);
|
|
188
|
+
}
|
|
59
189
|
return records;
|
|
60
190
|
}
|
|
61
191
|
|
|
@@ -82,7 +82,10 @@ async function detectExternalPlanningEdit(
|
|
|
82
82
|
_state: GSDState,
|
|
83
83
|
ctx: DriftContext,
|
|
84
84
|
): Promise<ExternalPlanningEditDrift[]> {
|
|
85
|
-
const marker = readCompatMarker(ctx.basePath
|
|
85
|
+
const marker = readCompatMarker(ctx.basePath, {
|
|
86
|
+
healInvalidKeys: !ctx.dryRun,
|
|
87
|
+
quarantineInvalid: !ctx.dryRun,
|
|
88
|
+
});
|
|
86
89
|
if (!marker.planning?.active) {
|
|
87
90
|
// Not yet activated. Activation (layout parse + DB import) is owned by
|
|
88
91
|
// capturePlanningCompatIfNeeded, called from reconcileBeforeDispatch when
|
|
@@ -16,7 +16,7 @@ import { renderRoadmapFromDb } from "../../markdown-renderer.js";
|
|
|
16
16
|
import { findMilestoneIds } from "../../milestone-ids.js";
|
|
17
17
|
import { parseRoadmap } from "../../parsers-legacy.js";
|
|
18
18
|
import { resolveMilestoneFile } from "../../paths.js";
|
|
19
|
-
import { isClosedStatus } from "../../status-guards.js";
|
|
19
|
+
import { isClosedStatus, isSkippedForDispatch } from "../../status-guards.js";
|
|
20
20
|
import type { GSDState } from "../../types.js";
|
|
21
21
|
import type { DriftContext, DriftHandler, DriftRecord } from "../types.js";
|
|
22
22
|
|
|
@@ -94,7 +94,9 @@ export function detectRoadmapDivergenceDrift(
|
|
|
94
94
|
for (const milestoneId of findMilestoneIds(ctx.basePath)) {
|
|
95
95
|
// Skip milestones that don't yet have a DB row — that's the
|
|
96
96
|
// unregistered-milestone drift handler's responsibility.
|
|
97
|
-
|
|
97
|
+
const milestone = getMilestone(milestoneId);
|
|
98
|
+
if (!milestone) continue;
|
|
99
|
+
if (isSkippedForDispatch(milestone.status)) continue;
|
|
98
100
|
if (milestoneHasDivergence(ctx.basePath, milestoneId)) {
|
|
99
101
|
drifts.push({ kind: "roadmap-divergence", milestoneId });
|
|
100
102
|
}
|
|
@@ -29,6 +29,14 @@ export function formatStopNoticePrefix(reason?: string | null): string {
|
|
|
29
29
|
return displayReason ? `${prefix} — ${displayReason}` : prefix;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
export function formatVerdictRecordedNotice(message: string): string {
|
|
33
|
+
return `Verdict recorded: ${message}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function formatVerdictRejectedNotice(message: string): string {
|
|
37
|
+
return `Verdict rejected: ${message}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
32
40
|
// ─── Classification (headless host side) ────────────────────────────────
|
|
33
41
|
// The canonical lowercase prefixes the headless event loop recognizes in
|
|
34
42
|
// notify messages. Emitters above and ad-hoc emitters elsewhere must start
|
|
@@ -42,6 +50,8 @@ export const TERMINAL_NOTICE_PREFIXES = [
|
|
|
42
50
|
"auto-mode complete",
|
|
43
51
|
"no active milestone",
|
|
44
52
|
"auto-mode idle",
|
|
53
|
+
"verdict recorded",
|
|
54
|
+
"verdict rejected",
|
|
45
55
|
] as const;
|
|
46
56
|
|
|
47
57
|
/** Manual-resolution notices emitted before auto-mode can formally pause/stop. */
|
|
@@ -85,6 +95,7 @@ export function isInteractiveMenuUnavailableNotice(message: string): boolean {
|
|
|
85
95
|
export function isBlockedNoticeMessage(message: string): boolean {
|
|
86
96
|
return (
|
|
87
97
|
message.includes("blocked:") ||
|
|
98
|
+
message.startsWith("verdict rejected") ||
|
|
88
99
|
(isPauseNotice(message) && !isNonBlockingPauseNotice(message)) ||
|
|
89
100
|
isManualResolutionNotice(message) ||
|
|
90
101
|
isInteractiveMenuUnavailableNotice(message)
|
|
@@ -41,6 +41,7 @@ import { claimMilestoneLease } from "../db/milestone-leases.js";
|
|
|
41
41
|
import { recordDispatchClaim, markCanceled } from "../db/unit-dispatches.js";
|
|
42
42
|
import { setRuntimeKv, getRuntimeKv } from "../db/runtime-kv.js";
|
|
43
43
|
import { SourceObservationStore } from "../source-observations.js";
|
|
44
|
+
import { autoCommitCurrentBranch } from "../worktree.js";
|
|
44
45
|
|
|
45
46
|
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
46
47
|
|
|
@@ -832,6 +833,37 @@ test("runUnit returns cancelled when session creation fails", async () => {
|
|
|
832
833
|
assert.equal(pi.calls.length, 0);
|
|
833
834
|
});
|
|
834
835
|
|
|
836
|
+
test("runUnit returns cancelled when command context lacks newSession", async () => {
|
|
837
|
+
_resetPendingResolve();
|
|
838
|
+
|
|
839
|
+
const ctx = {
|
|
840
|
+
...makeMockCtx(),
|
|
841
|
+
ui: {
|
|
842
|
+
notify: () => {},
|
|
843
|
+
setStatus: () => {},
|
|
844
|
+
setWorkingMessage: () => {},
|
|
845
|
+
},
|
|
846
|
+
sessionManager: {
|
|
847
|
+
getEntries: () => [],
|
|
848
|
+
},
|
|
849
|
+
modelRegistry: {
|
|
850
|
+
getProviderAuthMode: () => undefined,
|
|
851
|
+
isProviderRequestReady: () => true,
|
|
852
|
+
},
|
|
853
|
+
} as any;
|
|
854
|
+
const pi = makeMockPi();
|
|
855
|
+
const s = makeMockSession();
|
|
856
|
+
s.cmdCtx = {} as any;
|
|
857
|
+
|
|
858
|
+
const result = await runUnit(ctx, pi, s, "task", "T01", "prompt");
|
|
859
|
+
|
|
860
|
+
assert.equal(result.status, "cancelled");
|
|
861
|
+
assert.equal(result.errorContext?.category, "session-failed");
|
|
862
|
+
assert.equal(result.errorContext?.isTransient, false);
|
|
863
|
+
assert.match(result.errorContext?.message ?? "", /missing newSession/);
|
|
864
|
+
assert.equal(pi.calls.length, 0);
|
|
865
|
+
});
|
|
866
|
+
|
|
835
867
|
test("runUnit: TypeError from newSession is classified as structural (isTransient: false)", async () => {
|
|
836
868
|
// Regression for #572: a TypeError thrown from newSession (e.g. "something is
|
|
837
869
|
// not a function") indicates a programming error, not a transient provider
|
|
@@ -1582,6 +1614,130 @@ test("autoLoop exits when s.active is set to false", async (t) => {
|
|
|
1582
1614
|
);
|
|
1583
1615
|
});
|
|
1584
1616
|
|
|
1617
|
+
test("autoLoop stops before dispatch when command context lacks newSession", async () => {
|
|
1618
|
+
_resetPendingResolve();
|
|
1619
|
+
|
|
1620
|
+
const ctx = makeMockCtx();
|
|
1621
|
+
ctx.ui.setStatus = () => {};
|
|
1622
|
+
const pi = makeMockPi();
|
|
1623
|
+
const s = makeLoopSession({
|
|
1624
|
+
cmdCtx: {
|
|
1625
|
+
getContextUsage: () => ({ percent: 10, tokens: 1000, limit: 10000 }),
|
|
1626
|
+
},
|
|
1627
|
+
});
|
|
1628
|
+
let stopReason: string | undefined;
|
|
1629
|
+
let preserveWorktree: boolean | undefined;
|
|
1630
|
+
let deriveCalled = false;
|
|
1631
|
+
|
|
1632
|
+
try {
|
|
1633
|
+
const deps = makeMockDeps({
|
|
1634
|
+
stopAuto: async (_ctx, _pi, reason, options) => {
|
|
1635
|
+
stopReason = reason;
|
|
1636
|
+
preserveWorktree = options?.preserveWorktree;
|
|
1637
|
+
s.active = false;
|
|
1638
|
+
},
|
|
1639
|
+
deriveState: async () => {
|
|
1640
|
+
deriveCalled = true;
|
|
1641
|
+
throw new Error("deriveState should not run without command session support");
|
|
1642
|
+
},
|
|
1643
|
+
});
|
|
1644
|
+
|
|
1645
|
+
await autoLoop(ctx, pi, s, deps);
|
|
1646
|
+
|
|
1647
|
+
assert.equal(stopReason, "Auto-mode has no command context for dispatch.");
|
|
1648
|
+
assert.equal(preserveWorktree, true);
|
|
1649
|
+
assert.equal(deriveCalled, false);
|
|
1650
|
+
assert.equal(pi.calls.length, 0);
|
|
1651
|
+
} finally {
|
|
1652
|
+
rmSync(s.basePath, { recursive: true, force: true });
|
|
1653
|
+
}
|
|
1654
|
+
});
|
|
1655
|
+
|
|
1656
|
+
test("autoLoop commits open unit work when command context lacks newSession", async () => {
|
|
1657
|
+
_resetPendingResolve();
|
|
1658
|
+
|
|
1659
|
+
const ctx = makeMockCtx();
|
|
1660
|
+
ctx.ui.setStatus = () => {};
|
|
1661
|
+
const pi = makeMockPi();
|
|
1662
|
+
const s = makeLoopSession({
|
|
1663
|
+
cmdCtx: {
|
|
1664
|
+
getContextUsage: () => ({ percent: 10, tokens: 1000, limit: 10000 }),
|
|
1665
|
+
},
|
|
1666
|
+
currentUnit: { type: "execute-task", id: "T01", startedAt: Date.now() },
|
|
1667
|
+
});
|
|
1668
|
+
const outputPath = join(s.basePath, "executor-output.txt");
|
|
1669
|
+
let autoCommitArgs: { unitType: string; unitId: string } | undefined;
|
|
1670
|
+
let preserveWorktree: boolean | undefined;
|
|
1671
|
+
|
|
1672
|
+
try {
|
|
1673
|
+
const deps = makeMockDeps({
|
|
1674
|
+
autoCommitUnit: async (basePath, unitType, unitId) => {
|
|
1675
|
+
autoCommitArgs = { unitType, unitId };
|
|
1676
|
+
return autoCommitCurrentBranch(basePath, unitType, unitId);
|
|
1677
|
+
},
|
|
1678
|
+
stopAuto: async (_ctx, _pi, _reason, options) => {
|
|
1679
|
+
preserveWorktree = options?.preserveWorktree;
|
|
1680
|
+
s.active = false;
|
|
1681
|
+
},
|
|
1682
|
+
deriveState: async () => {
|
|
1683
|
+
throw new Error("deriveState should not run without command session support");
|
|
1684
|
+
},
|
|
1685
|
+
});
|
|
1686
|
+
|
|
1687
|
+
writeFileSync(outputPath, "open unit work before stop\n", "utf-8");
|
|
1688
|
+
|
|
1689
|
+
await autoLoop(ctx, pi, s, deps);
|
|
1690
|
+
|
|
1691
|
+
assert.deepEqual(autoCommitArgs, { unitType: "execute-task", unitId: "T01" });
|
|
1692
|
+
assert.equal(preserveWorktree, true);
|
|
1693
|
+
const committed = execSync("git show HEAD:executor-output.txt", {
|
|
1694
|
+
cwd: s.basePath,
|
|
1695
|
+
encoding: "utf-8",
|
|
1696
|
+
});
|
|
1697
|
+
assert.equal(committed, "open unit work before stop\n");
|
|
1698
|
+
assert.equal(pi.calls.length, 0);
|
|
1699
|
+
} finally {
|
|
1700
|
+
rmSync(s.basePath, { recursive: true, force: true });
|
|
1701
|
+
}
|
|
1702
|
+
});
|
|
1703
|
+
|
|
1704
|
+
test("autoLoop snapshots dirty work when unit dispatch crashes", async () => {
|
|
1705
|
+
_resetPendingResolve();
|
|
1706
|
+
|
|
1707
|
+
const ctx = makeMockCtx();
|
|
1708
|
+
ctx.ui.setStatus = () => {};
|
|
1709
|
+
const pi = makeMockPi();
|
|
1710
|
+
const s = makeLoopSession();
|
|
1711
|
+
const outputPath = join(s.basePath, "executor-output.txt");
|
|
1712
|
+
let autoCommitCalls = 0;
|
|
1713
|
+
|
|
1714
|
+
try {
|
|
1715
|
+
const deps = makeMockDeps({
|
|
1716
|
+
ensurePreconditions: () => {
|
|
1717
|
+
writeFileSync(outputPath, "saved before crash\n", "utf-8");
|
|
1718
|
+
throw new Error("dispatch crash after file write");
|
|
1719
|
+
},
|
|
1720
|
+
autoCommitUnit: async (basePath, unitType, unitId) => {
|
|
1721
|
+
autoCommitCalls++;
|
|
1722
|
+
const commitMsg = autoCommitCurrentBranch(basePath, unitType, unitId);
|
|
1723
|
+
s.active = false;
|
|
1724
|
+
return commitMsg;
|
|
1725
|
+
},
|
|
1726
|
+
});
|
|
1727
|
+
|
|
1728
|
+
await autoLoop(ctx, pi, s, deps);
|
|
1729
|
+
|
|
1730
|
+
assert.equal(autoCommitCalls, 1);
|
|
1731
|
+
const committed = execSync("git show HEAD:executor-output.txt", {
|
|
1732
|
+
cwd: s.basePath,
|
|
1733
|
+
encoding: "utf-8",
|
|
1734
|
+
});
|
|
1735
|
+
assert.equal(committed, "saved before crash\n");
|
|
1736
|
+
} finally {
|
|
1737
|
+
rmSync(s.basePath, { recursive: true, force: true });
|
|
1738
|
+
}
|
|
1739
|
+
});
|
|
1740
|
+
|
|
1585
1741
|
test("autoLoop pauses visibly when Auto Orchestration Module is not wired", async () => {
|
|
1586
1742
|
_resetPendingResolve();
|
|
1587
1743
|
|
|
@@ -5421,6 +5577,114 @@ test("runUnitPhase retries 0-tool units with ordinary network-related assistant
|
|
|
5421
5577
|
assert.equal(deps.callLog.includes("pauseAuto"), false);
|
|
5422
5578
|
});
|
|
5423
5579
|
|
|
5580
|
+
test("runUnitPhase pauses 0-tool units with pseudo tool-call text as serialization drift", async (t) => {
|
|
5581
|
+
_resetPendingResolve();
|
|
5582
|
+
|
|
5583
|
+
const basePath = makeLoopTestBase("gsd-zero-tool-pseudo-tool-");
|
|
5584
|
+
t.after(() => {
|
|
5585
|
+
rmSync(basePath, { recursive: true, force: true });
|
|
5586
|
+
});
|
|
5587
|
+
|
|
5588
|
+
const notifications: string[] = [];
|
|
5589
|
+
const ctx = {
|
|
5590
|
+
...makeMockCtx(),
|
|
5591
|
+
ui: {
|
|
5592
|
+
notify: (msg: string) => { notifications.push(msg); },
|
|
5593
|
+
setStatus: () => {},
|
|
5594
|
+
setWorkingMessage: () => {},
|
|
5595
|
+
},
|
|
5596
|
+
sessionManager: {
|
|
5597
|
+
getEntries: () => [],
|
|
5598
|
+
},
|
|
5599
|
+
modelRegistry: {
|
|
5600
|
+
getProviderAuthMode: () => undefined,
|
|
5601
|
+
isProviderRequestReady: () => true,
|
|
5602
|
+
},
|
|
5603
|
+
} as any;
|
|
5604
|
+
const pi = {
|
|
5605
|
+
...makeMockPi(),
|
|
5606
|
+
sendMessage: () => {
|
|
5607
|
+
queueMicrotask(() => resolveAgentEnd(makeEvent([
|
|
5608
|
+
{
|
|
5609
|
+
role: "assistant",
|
|
5610
|
+
content: [
|
|
5611
|
+
{
|
|
5612
|
+
type: "text",
|
|
5613
|
+
text: 'bash<arg_key>command</arg_key><arg_value>ls -la /tmp && echo "---SRC---"</arg_value></tool_call>',
|
|
5614
|
+
},
|
|
5615
|
+
],
|
|
5616
|
+
},
|
|
5617
|
+
])));
|
|
5618
|
+
},
|
|
5619
|
+
} as any;
|
|
5620
|
+
const s = makeLoopSession({
|
|
5621
|
+
basePath,
|
|
5622
|
+
canonicalProjectRoot: basePath,
|
|
5623
|
+
originalBasePath: basePath,
|
|
5624
|
+
});
|
|
5625
|
+
const mockLedger = {
|
|
5626
|
+
version: 1,
|
|
5627
|
+
projectStartedAt: Date.now(),
|
|
5628
|
+
units: [] as any[],
|
|
5629
|
+
};
|
|
5630
|
+
const deps = makeMockDeps({
|
|
5631
|
+
closeoutUnit: async () => {
|
|
5632
|
+
mockLedger.units.push({
|
|
5633
|
+
type: "execute-task",
|
|
5634
|
+
id: "M001/S01/T01",
|
|
5635
|
+
startedAt: s.currentUnit?.startedAt ?? Date.now(),
|
|
5636
|
+
toolCalls: 0,
|
|
5637
|
+
assistantMessages: 1,
|
|
5638
|
+
tokens: { input: 100, output: 20, total: 120, cacheRead: 0, cacheWrite: 0 },
|
|
5639
|
+
cost: 0.01,
|
|
5640
|
+
});
|
|
5641
|
+
},
|
|
5642
|
+
getLedger: () => mockLedger,
|
|
5643
|
+
});
|
|
5644
|
+
let seq = 0;
|
|
5645
|
+
|
|
5646
|
+
const result = await runUnitPhase(
|
|
5647
|
+
{ ctx, pi, s, deps, prefs: undefined, iteration: 1, flowId: "flow-zero-tool-pseudo-tool", nextSeq: () => ++seq },
|
|
5648
|
+
{
|
|
5649
|
+
unitType: "execute-task",
|
|
5650
|
+
unitId: "M001/S01/T01",
|
|
5651
|
+
prompt: "do work",
|
|
5652
|
+
finalPrompt: "do work",
|
|
5653
|
+
pauseAfterUatDispatch: false,
|
|
5654
|
+
state: {
|
|
5655
|
+
phase: "executing",
|
|
5656
|
+
activeMilestone: { id: "M001", title: "Milestone" },
|
|
5657
|
+
activeSlice: { id: "S01", title: "Slice" },
|
|
5658
|
+
activeTask: { id: "T01", title: "Task" },
|
|
5659
|
+
registry: [{ id: "M001", title: "Milestone", status: "active" }],
|
|
5660
|
+
recentDecisions: [],
|
|
5661
|
+
blockers: [],
|
|
5662
|
+
nextAction: "",
|
|
5663
|
+
progress: { milestones: { done: 0, total: 1 } },
|
|
5664
|
+
requirements: { active: 0, validated: 0, deferred: 0, outOfScope: 0, blocked: 0, total: 0 },
|
|
5665
|
+
} as any,
|
|
5666
|
+
mid: "M001",
|
|
5667
|
+
midTitle: "Milestone",
|
|
5668
|
+
isRetry: false,
|
|
5669
|
+
previousTier: undefined,
|
|
5670
|
+
},
|
|
5671
|
+
{ recentUnits: [{ key: "execute-task/M001/S01/T01" }], stuckRecoveryAttempts: 0, consecutiveFinalizeTimeouts: 0 },
|
|
5672
|
+
);
|
|
5673
|
+
|
|
5674
|
+
assert.equal(result.action, "break");
|
|
5675
|
+
assert.equal((result as any).reason, "zero-tool-serialization-drift");
|
|
5676
|
+
assert.equal(deps.callLog.includes("pauseAuto"), true);
|
|
5677
|
+
assert.equal(s.zeroToolRetryCount.has("execute-task/M001/S01/T01"), false);
|
|
5678
|
+
assert.ok(
|
|
5679
|
+
notifications.some((msg) => msg.includes("serialization drift") && msg.includes("bash<arg_key>command")),
|
|
5680
|
+
"serialization drift notification should include a snippet of the pseudo tool-call text",
|
|
5681
|
+
);
|
|
5682
|
+
assert.ok(
|
|
5683
|
+
!notifications.some((msg) => msg.includes("context exhaustion")),
|
|
5684
|
+
"pseudo tool-call text should not be labeled as context exhaustion",
|
|
5685
|
+
);
|
|
5686
|
+
});
|
|
5687
|
+
|
|
5424
5688
|
test("runUnitPhase pauses auto-mode when zero-tool-call retry is exhausted", async (t) => {
|
|
5425
5689
|
_resetPendingResolve();
|
|
5426
5690
|
|
|
@@ -4,7 +4,7 @@ import { existsSync, mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:
|
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
6
|
|
|
7
|
-
import { hasGitIndexLockForTest } from "../auto-start.ts";
|
|
7
|
+
import { hasGitIndexLockForTest, reconcileFlatPhaseBootstrapLayout } from "../auto-start.ts";
|
|
8
8
|
|
|
9
9
|
test("bootstrapAutoSession detects .git/index.lock without deleting it", (t) => {
|
|
10
10
|
const base = mkdtempSync(join(tmpdir(), "gsd-index-lock-"));
|
|
@@ -24,3 +24,23 @@ test("bootstrapAutoSession reports no index lock when the lock file is absent",
|
|
|
24
24
|
|
|
25
25
|
assert.equal(hasGitIndexLockForTest(base), false);
|
|
26
26
|
});
|
|
27
|
+
|
|
28
|
+
test("bootstrapAutoSession removes empty legacy milestones dir in flat-phase projects", (t) => {
|
|
29
|
+
const base = mkdtempSync(join(tmpdir(), "gsd-flat-bootstrap-clean-"));
|
|
30
|
+
t.after(() => rmSync(base, { recursive: true, force: true }));
|
|
31
|
+
mkdirSync(join(base, ".gsd", "phases"), { recursive: true });
|
|
32
|
+
mkdirSync(join(base, ".gsd", "milestones"), { recursive: true });
|
|
33
|
+
|
|
34
|
+
assert.equal(reconcileFlatPhaseBootstrapLayout(base), true);
|
|
35
|
+
assert.equal(existsSync(join(base, ".gsd", "phases")), true);
|
|
36
|
+
assert.equal(existsSync(join(base, ".gsd", "milestones")), false);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("bootstrapAutoSession creates phases instead of milestones for empty GSD state", (t) => {
|
|
40
|
+
const base = mkdtempSync(join(tmpdir(), "gsd-flat-bootstrap-init-"));
|
|
41
|
+
t.after(() => rmSync(base, { recursive: true, force: true }));
|
|
42
|
+
|
|
43
|
+
assert.equal(reconcileFlatPhaseBootstrapLayout(base), true);
|
|
44
|
+
assert.equal(existsSync(join(base, ".gsd", "phases")), true);
|
|
45
|
+
assert.equal(existsSync(join(base, ".gsd", "milestones")), false);
|
|
46
|
+
});
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
|
|
4
4
|
import { describe, test } from "node:test";
|
|
5
5
|
import assert from "node:assert/strict";
|
|
6
|
-
import { mkdtempSync, mkdirSync, realpathSync, rmSync } from "node:fs";
|
|
6
|
+
import { mkdtempSync, mkdirSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { execSync } from "node:child_process";
|
|
7
8
|
import { join } from "node:path";
|
|
8
9
|
import { tmpdir } from "node:os";
|
|
9
10
|
|
|
@@ -96,6 +97,78 @@ describe("hook dispatch session workspace root", () => {
|
|
|
96
97
|
assert.equal(dispatched, true);
|
|
97
98
|
assert.deepEqual(newSessionOptions, { workspaceRoot: basePath });
|
|
98
99
|
});
|
|
100
|
+
|
|
101
|
+
test("dispatchHookUnit commits in-flight work when command context lacks newSession", async (t) => {
|
|
102
|
+
const originalCwd = process.cwd();
|
|
103
|
+
const basePath = realpathSync(mkdtempSync(join(tmpdir(), "gsd-hook-nosnap-")));
|
|
104
|
+
mkdirSync(join(basePath, ".gsd"), { recursive: true });
|
|
105
|
+
execSync("git init --initial-branch=main", { cwd: basePath, stdio: "ignore" });
|
|
106
|
+
execSync("git config user.email test@test.com", { cwd: basePath, stdio: "ignore" });
|
|
107
|
+
execSync("git config user.name Test", { cwd: basePath, stdio: "ignore" });
|
|
108
|
+
execSync("git commit --allow-empty -m init", { cwd: basePath, stdio: "ignore" });
|
|
109
|
+
|
|
110
|
+
autoSession.reset();
|
|
111
|
+
autoSession.active = true;
|
|
112
|
+
autoSession.basePath = basePath;
|
|
113
|
+
autoSession.originalBasePath = basePath;
|
|
114
|
+
// Simulate an in-flight unit whose executor already wrote files, then a hook
|
|
115
|
+
// fires with a command context missing newSession.
|
|
116
|
+
autoSession.currentUnit = {
|
|
117
|
+
type: "execute-task",
|
|
118
|
+
id: "M001/S01/T01",
|
|
119
|
+
startedAt: Date.now(),
|
|
120
|
+
};
|
|
121
|
+
t.after(() => {
|
|
122
|
+
try {
|
|
123
|
+
process.chdir(originalCwd);
|
|
124
|
+
} catch {
|
|
125
|
+
// best effort cleanup after cwd-sensitive dispatch tests
|
|
126
|
+
}
|
|
127
|
+
autoSession.reset();
|
|
128
|
+
rmSync(basePath, { recursive: true, force: true });
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const outputPath = join(basePath, "executor-output.txt");
|
|
132
|
+
writeFileSync(outputPath, "in-flight executor work\n", "utf-8");
|
|
133
|
+
|
|
134
|
+
// Command context intentionally lacks newSession — the guard must snapshot
|
|
135
|
+
// the dirty work before stopping instead of discarding it.
|
|
136
|
+
const ctx = {
|
|
137
|
+
ui: {
|
|
138
|
+
notify: () => {},
|
|
139
|
+
setStatus: () => {},
|
|
140
|
+
setWidget: () => {},
|
|
141
|
+
},
|
|
142
|
+
modelRegistry: {
|
|
143
|
+
getAvailable: () => [],
|
|
144
|
+
},
|
|
145
|
+
sessionManager: {
|
|
146
|
+
getSessionFile: () => join(basePath, "session.jsonl"),
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
const pi = {
|
|
150
|
+
sendMessage: () => {},
|
|
151
|
+
setModel: async () => true,
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const dispatched = await dispatchHookUnit(
|
|
155
|
+
ctx as any,
|
|
156
|
+
pi as any,
|
|
157
|
+
"review",
|
|
158
|
+
"execute-task",
|
|
159
|
+
"M001/S01/T01",
|
|
160
|
+
"review the completed unit",
|
|
161
|
+
undefined,
|
|
162
|
+
basePath,
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
assert.equal(dispatched, false);
|
|
166
|
+
const committed = execSync("git show HEAD:executor-output.txt", {
|
|
167
|
+
cwd: basePath,
|
|
168
|
+
encoding: "utf-8",
|
|
169
|
+
});
|
|
170
|
+
assert.equal(committed, "in-flight executor work\n");
|
|
171
|
+
});
|
|
99
172
|
});
|
|
100
173
|
|
|
101
174
|
describe("deep setup approval questions pause immediately", () => {
|
|
@@ -201,6 +201,7 @@ test("handleVerdict rejects missing verdict", async () => {
|
|
|
201
201
|
const { ctx, calls } = makeMockCtx();
|
|
202
202
|
await handleVerdict("", ctx, "/tmp/unused");
|
|
203
203
|
assert.equal(calls.length, 1);
|
|
204
|
+
assert.match(calls[0].message, /^Verdict rejected:/);
|
|
204
205
|
assert.match(calls[0].message, /Usage: \/gsd verdict/);
|
|
205
206
|
assert.equal(calls[0].kind, "warning");
|
|
206
207
|
});
|
|
@@ -209,6 +210,7 @@ test("handleVerdict rejects invalid verdict", async () => {
|
|
|
209
210
|
const { ctx, calls } = makeMockCtx();
|
|
210
211
|
await handleVerdict("yolo", ctx, "/tmp/unused");
|
|
211
212
|
assert.equal(calls.length, 1);
|
|
213
|
+
assert.match(calls[0].message, /^Verdict rejected:/);
|
|
212
214
|
assert.match(calls[0].message, /Invalid verdict "yolo"/);
|
|
213
215
|
assert.equal(calls[0].kind, "warning");
|
|
214
216
|
});
|
|
@@ -250,6 +252,10 @@ test("handleVerdict rejects when milestone validation is missing", async () => {
|
|
|
250
252
|
calls.some((c) => /No milestone validation found/.test(c.message)),
|
|
251
253
|
`expected missing-validation warning, got: ${JSON.stringify(calls)}`,
|
|
252
254
|
);
|
|
255
|
+
assert.ok(
|
|
256
|
+
calls.some((c) => /^Verdict rejected:/.test(c.message)),
|
|
257
|
+
`expected terminal rejection prefix, got: ${JSON.stringify(calls)}`,
|
|
258
|
+
);
|
|
253
259
|
} finally {
|
|
254
260
|
closeDatabase();
|
|
255
261
|
invalidateStateCache();
|
|
@@ -280,6 +286,10 @@ test("handleVerdict pass override flips verdict and preserves sections", async (
|
|
|
280
286
|
calls.some((c) => c.kind === "success" && /needs-attention.*->.*pass/.test(c.message)),
|
|
281
287
|
`expected success notification, got: ${JSON.stringify(calls)}`,
|
|
282
288
|
);
|
|
289
|
+
assert.ok(
|
|
290
|
+
calls.some((c) => c.kind === "success" && /^Verdict recorded:/.test(c.message)),
|
|
291
|
+
`expected terminal recorded prefix, got: ${JSON.stringify(calls)}`,
|
|
292
|
+
);
|
|
283
293
|
} finally {
|
|
284
294
|
closeDatabase();
|
|
285
295
|
invalidateStateCache();
|