gsd-pi 2.37.1 → 2.38.0-dev.4d4d14a
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/app-paths.js +1 -1
- package/dist/cli.js +9 -0
- package/dist/extension-discovery.d.ts +5 -3
- package/dist/extension-discovery.js +14 -9
- package/dist/extension-registry.js +2 -2
- package/dist/onboarding.js +1 -0
- package/dist/remote-questions-config.js +2 -2
- package/dist/resources/extensions/browser-tools/package.json +3 -1
- package/dist/resources/extensions/cmux/index.js +55 -1
- package/dist/resources/extensions/context7/package.json +1 -1
- package/dist/resources/extensions/env-utils.js +29 -0
- package/dist/resources/extensions/get-secrets-from-user.js +5 -24
- package/dist/resources/extensions/github-sync/cli.js +284 -0
- package/dist/resources/extensions/github-sync/index.js +73 -0
- package/dist/resources/extensions/github-sync/mapping.js +67 -0
- package/dist/resources/extensions/github-sync/sync.js +424 -0
- package/dist/resources/extensions/github-sync/templates.js +118 -0
- package/dist/resources/extensions/github-sync/types.js +7 -0
- package/dist/resources/extensions/google-search/package.json +3 -1
- package/dist/resources/extensions/gsd/auto/session.js +6 -23
- package/dist/resources/extensions/gsd/auto-dispatch.js +74 -9
- package/dist/resources/extensions/gsd/auto-loop.js +149 -170
- package/dist/resources/extensions/gsd/auto-post-unit.js +105 -68
- package/dist/resources/extensions/gsd/auto-prompts.js +98 -33
- package/dist/resources/extensions/gsd/auto-recovery.js +37 -1
- package/dist/resources/extensions/gsd/auto-start.js +13 -2
- package/dist/resources/extensions/gsd/auto-worktree-sync.js +13 -5
- package/dist/resources/extensions/gsd/auto.js +143 -96
- package/dist/resources/extensions/gsd/captures.js +9 -1
- package/dist/resources/extensions/gsd/commands-extensions.js +3 -2
- package/dist/resources/extensions/gsd/commands-handlers.js +16 -3
- package/dist/resources/extensions/gsd/commands-prefs-wizard.js +1 -1
- package/dist/resources/extensions/gsd/commands.js +22 -2
- package/dist/resources/extensions/gsd/context-budget.js +2 -10
- package/dist/resources/extensions/gsd/detection.js +1 -2
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +0 -2
- package/dist/resources/extensions/gsd/doctor-checks.js +82 -0
- package/dist/resources/extensions/gsd/doctor-environment.js +78 -0
- package/dist/resources/extensions/gsd/doctor-format.js +15 -0
- package/dist/resources/extensions/gsd/doctor-providers.js +62 -12
- package/dist/resources/extensions/gsd/doctor.js +184 -11
- package/dist/resources/extensions/gsd/export.js +1 -1
- package/dist/resources/extensions/gsd/files.js +43 -2
- package/dist/resources/extensions/gsd/forensics.js +1 -1
- package/dist/resources/extensions/gsd/git-service.js +8 -1
- package/dist/resources/extensions/gsd/index.js +24 -20
- package/dist/resources/extensions/gsd/migrate/parsers.js +1 -1
- package/dist/resources/extensions/gsd/observability-validator.js +24 -0
- package/dist/resources/extensions/gsd/package.json +1 -1
- package/dist/resources/extensions/gsd/preferences-models.js +0 -12
- package/dist/resources/extensions/gsd/preferences-types.js +3 -2
- package/dist/resources/extensions/gsd/preferences-validation.js +101 -11
- package/dist/resources/extensions/gsd/preferences.js +8 -5
- package/dist/resources/extensions/gsd/prompts/discuss.md +11 -14
- package/dist/resources/extensions/gsd/prompts/execute-task.md +2 -2
- package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +11 -12
- package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -10
- package/dist/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
- package/dist/resources/extensions/gsd/prompts/plan-slice.md +2 -1
- package/dist/resources/extensions/gsd/prompts/queue.md +4 -8
- package/dist/resources/extensions/gsd/prompts/reactive-execute.md +44 -0
- package/dist/resources/extensions/gsd/prompts/run-uat.md +25 -10
- package/dist/resources/extensions/gsd/prompts/workflow-start.md +2 -2
- package/dist/resources/extensions/gsd/reactive-graph.js +227 -0
- package/dist/resources/extensions/gsd/repo-identity.js +21 -4
- package/dist/resources/extensions/gsd/resource-version.js +2 -1
- package/dist/resources/extensions/gsd/state.js +1 -1
- package/dist/resources/extensions/gsd/templates/task-plan.md +11 -3
- package/dist/resources/extensions/gsd/visualizer-data.js +1 -1
- package/dist/resources/extensions/gsd/worktree.js +35 -16
- package/dist/resources/extensions/remote-questions/status.js +2 -1
- package/dist/resources/extensions/remote-questions/store.js +2 -1
- package/dist/resources/extensions/search-the-web/provider.js +2 -1
- package/dist/resources/extensions/subagent/index.js +12 -3
- package/dist/resources/extensions/subagent/isolation.js +2 -1
- package/dist/resources/extensions/ttsr/rule-loader.js +2 -1
- package/dist/resources/extensions/universal-config/package.json +1 -1
- package/dist/welcome-screen.d.ts +12 -0
- package/dist/welcome-screen.js +53 -0
- package/package.json +2 -1
- package/packages/pi-ai/dist/env-api-keys.js +13 -0
- package/packages/pi-ai/dist/env-api-keys.js.map +1 -1
- package/packages/pi-ai/dist/models.generated.d.ts +172 -0
- package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
- package/packages/pi-ai/dist/models.generated.js +172 -0
- package/packages/pi-ai/dist/models.generated.js.map +1 -1
- package/packages/pi-ai/dist/providers/anthropic-shared.d.ts +64 -0
- package/packages/pi-ai/dist/providers/anthropic-shared.d.ts.map +1 -0
- package/packages/pi-ai/dist/providers/anthropic-shared.js +668 -0
- package/packages/pi-ai/dist/providers/anthropic-shared.js.map +1 -0
- package/packages/pi-ai/dist/providers/anthropic-vertex.d.ts +5 -0
- package/packages/pi-ai/dist/providers/anthropic-vertex.d.ts.map +1 -0
- package/packages/pi-ai/dist/providers/anthropic-vertex.js +85 -0
- package/packages/pi-ai/dist/providers/anthropic-vertex.js.map +1 -0
- package/packages/pi-ai/dist/providers/anthropic.d.ts +4 -30
- package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/anthropic.js +47 -764
- package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
- package/packages/pi-ai/dist/providers/register-builtins.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/register-builtins.js +6 -0
- package/packages/pi-ai/dist/providers/register-builtins.js.map +1 -1
- package/packages/pi-ai/dist/types.d.ts +2 -2
- package/packages/pi-ai/dist/types.d.ts.map +1 -1
- package/packages/pi-ai/dist/types.js.map +1 -1
- package/packages/pi-ai/dist/utils/oauth/anthropic.js +2 -2
- package/packages/pi-ai/dist/utils/oauth/anthropic.js.map +1 -1
- package/packages/pi-ai/package.json +1 -0
- package/packages/pi-ai/src/env-api-keys.ts +14 -0
- package/packages/pi-ai/src/models.generated.ts +172 -0
- package/packages/pi-ai/src/providers/anthropic-shared.ts +761 -0
- package/packages/pi-ai/src/providers/anthropic-vertex.ts +130 -0
- package/packages/pi-ai/src/providers/anthropic.ts +76 -868
- package/packages/pi-ai/src/providers/register-builtins.ts +7 -0
- package/packages/pi-ai/src/types.ts +2 -0
- package/packages/pi-ai/src/utils/oauth/anthropic.ts +2 -2
- package/packages/pi-coding-agent/dist/core/model-resolver.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-resolver.js +1 -0
- package/packages/pi-coding-agent/dist/core/model-resolver.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/package-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/package-manager.js +8 -4
- package/packages/pi-coding-agent/dist/core/package-manager.js.map +1 -1
- package/packages/pi-coding-agent/package.json +1 -1
- package/packages/pi-coding-agent/src/core/model-resolver.ts +1 -0
- package/packages/pi-coding-agent/src/core/package-manager.ts +8 -4
- package/pkg/package.json +1 -1
- package/src/resources/extensions/cmux/index.ts +57 -1
- package/src/resources/extensions/env-utils.ts +31 -0
- package/src/resources/extensions/get-secrets-from-user.ts +5 -24
- package/src/resources/extensions/github-sync/cli.ts +364 -0
- package/src/resources/extensions/github-sync/index.ts +93 -0
- package/src/resources/extensions/github-sync/mapping.ts +81 -0
- package/src/resources/extensions/github-sync/sync.ts +556 -0
- package/src/resources/extensions/github-sync/templates.ts +183 -0
- package/src/resources/extensions/github-sync/tests/cli.test.ts +20 -0
- package/src/resources/extensions/github-sync/tests/commit-linking.test.ts +39 -0
- package/src/resources/extensions/github-sync/tests/mapping.test.ts +104 -0
- package/src/resources/extensions/github-sync/tests/templates.test.ts +110 -0
- package/src/resources/extensions/github-sync/types.ts +47 -0
- package/src/resources/extensions/gsd/auto/session.ts +7 -25
- package/src/resources/extensions/gsd/auto-dispatch.ts +99 -8
- package/src/resources/extensions/gsd/auto-loop.ts +207 -252
- package/src/resources/extensions/gsd/auto-post-unit.ts +82 -39
- package/src/resources/extensions/gsd/auto-prompts.ts +132 -36
- package/src/resources/extensions/gsd/auto-recovery.ts +42 -0
- package/src/resources/extensions/gsd/auto-start.ts +18 -2
- package/src/resources/extensions/gsd/auto-worktree-sync.ts +15 -4
- package/src/resources/extensions/gsd/auto.ts +139 -101
- package/src/resources/extensions/gsd/captures.ts +10 -1
- package/src/resources/extensions/gsd/commands-extensions.ts +4 -2
- package/src/resources/extensions/gsd/commands-handlers.ts +17 -2
- package/src/resources/extensions/gsd/commands-prefs-wizard.ts +1 -1
- package/src/resources/extensions/gsd/commands.ts +24 -2
- package/src/resources/extensions/gsd/context-budget.ts +2 -12
- package/src/resources/extensions/gsd/detection.ts +2 -2
- package/src/resources/extensions/gsd/docs/preferences-reference.md +0 -2
- package/src/resources/extensions/gsd/doctor-checks.ts +75 -0
- package/src/resources/extensions/gsd/doctor-environment.ts +82 -1
- package/src/resources/extensions/gsd/doctor-format.ts +20 -0
- package/src/resources/extensions/gsd/doctor-providers.ts +64 -10
- package/src/resources/extensions/gsd/doctor-types.ts +16 -1
- package/src/resources/extensions/gsd/doctor.ts +177 -13
- package/src/resources/extensions/gsd/export.ts +1 -1
- package/src/resources/extensions/gsd/files.ts +47 -2
- package/src/resources/extensions/gsd/forensics.ts +1 -1
- package/src/resources/extensions/gsd/git-service.ts +13 -1
- package/src/resources/extensions/gsd/index.ts +24 -17
- package/src/resources/extensions/gsd/migrate/parsers.ts +1 -1
- package/src/resources/extensions/gsd/observability-validator.ts +27 -0
- package/src/resources/extensions/gsd/preferences-models.ts +0 -12
- package/src/resources/extensions/gsd/preferences-types.ts +9 -5
- package/src/resources/extensions/gsd/preferences-validation.ts +92 -11
- package/src/resources/extensions/gsd/preferences.ts +8 -5
- package/src/resources/extensions/gsd/prompts/discuss.md +11 -14
- package/src/resources/extensions/gsd/prompts/execute-task.md +2 -2
- package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +11 -12
- package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -10
- package/src/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
- package/src/resources/extensions/gsd/prompts/plan-slice.md +2 -1
- package/src/resources/extensions/gsd/prompts/queue.md +4 -8
- package/src/resources/extensions/gsd/prompts/reactive-execute.md +44 -0
- package/src/resources/extensions/gsd/prompts/run-uat.md +25 -10
- package/src/resources/extensions/gsd/prompts/workflow-start.md +2 -2
- package/src/resources/extensions/gsd/reactive-graph.ts +289 -0
- package/src/resources/extensions/gsd/repo-identity.ts +23 -4
- package/src/resources/extensions/gsd/resource-version.ts +3 -1
- package/src/resources/extensions/gsd/state.ts +1 -1
- package/src/resources/extensions/gsd/templates/task-plan.md +11 -3
- package/src/resources/extensions/gsd/tests/agent-end-retry.test.ts +21 -18
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +16 -37
- package/src/resources/extensions/gsd/tests/cmux.test.ts +93 -0
- package/src/resources/extensions/gsd/tests/doctor-enhancements.test.ts +266 -0
- package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +191 -3
- package/src/resources/extensions/gsd/tests/plan-quality-validator.test.ts +111 -0
- package/src/resources/extensions/gsd/tests/preferences.test.ts +2 -7
- package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +59 -0
- package/src/resources/extensions/gsd/tests/reactive-executor.test.ts +511 -0
- package/src/resources/extensions/gsd/tests/reactive-graph.test.ts +299 -0
- package/src/resources/extensions/gsd/tests/repo-identity-worktree.test.ts +21 -1
- package/src/resources/extensions/gsd/tests/run-uat.test.ts +11 -3
- package/src/resources/extensions/gsd/tests/worktree.test.ts +47 -0
- package/src/resources/extensions/gsd/types.ts +43 -1
- package/src/resources/extensions/gsd/visualizer-data.ts +1 -1
- package/src/resources/extensions/gsd/worktree.ts +35 -15
- package/src/resources/extensions/remote-questions/status.ts +3 -1
- package/src/resources/extensions/remote-questions/store.ts +3 -1
- package/src/resources/extensions/search-the-web/provider.ts +2 -1
- package/src/resources/extensions/subagent/index.ts +12 -3
- package/src/resources/extensions/subagent/isolation.ts +3 -1
- package/src/resources/extensions/ttsr/rule-loader.ts +3 -1
- package/dist/resources/extensions/gsd/prompt-compressor.js +0 -393
- package/dist/resources/extensions/gsd/semantic-chunker.js +0 -254
- package/dist/resources/extensions/gsd/summary-distiller.js +0 -212
- package/src/resources/extensions/gsd/prompt-compressor.ts +0 -508
- package/src/resources/extensions/gsd/semantic-chunker.ts +0 -336
- package/src/resources/extensions/gsd/summary-distiller.ts +0 -258
- package/src/resources/extensions/gsd/tests/context-compression.test.ts +0 -193
- package/src/resources/extensions/gsd/tests/prompt-compressor.test.ts +0 -529
- package/src/resources/extensions/gsd/tests/semantic-chunker.test.ts +0 -426
- package/src/resources/extensions/gsd/tests/summary-distiller.test.ts +0 -323
- package/src/resources/extensions/gsd/tests/token-optimization-benchmark.test.ts +0 -1272
- package/src/resources/extensions/gsd/tests/token-optimization-prefs.test.ts +0 -164
|
@@ -42,11 +42,19 @@ estimated_files: {{estimatedFiles}}
|
|
|
42
42
|
|
|
43
43
|
## Inputs
|
|
44
44
|
|
|
45
|
+
<!-- Every input MUST be a backtick-wrapped file path. These paths are machine-parsed to
|
|
46
|
+
derive task dependencies — vague descriptions without paths break dependency detection.
|
|
47
|
+
For the first task in a slice with no prior task outputs, list the existing source files
|
|
48
|
+
this task reads or modifies. -->
|
|
49
|
+
|
|
45
50
|
- `{{filePath}}` — {{whatThisTaskNeedsFromPriorWork}}
|
|
46
|
-
- {{priorTaskSummaryInsight}}
|
|
47
51
|
|
|
48
52
|
## Expected Output
|
|
49
53
|
|
|
50
|
-
<!--
|
|
54
|
+
<!-- Every output MUST be a backtick-wrapped file path — the specific files this task creates
|
|
55
|
+
or modifies. These paths are machine-parsed to derive task dependencies.
|
|
56
|
+
This task should produce a real increment toward making the slice goal/demo true. A full
|
|
57
|
+
slice plan should not be able to mark every task complete while the claimed slice behavior
|
|
58
|
+
still does not work at the stated proof level. -->
|
|
51
59
|
|
|
52
|
-
- `{{filePath}}` — {{
|
|
60
|
+
- `{{filePath}}` — {{whatThisTaskCreatesOrModifies}}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* agent-end-retry.test.ts — Regression checks for the
|
|
2
|
+
* agent-end-retry.test.ts — Regression checks for the agent_end model.
|
|
3
3
|
*
|
|
4
|
-
* The
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* The per-unit one-shot resolve function lives at module level in auto-loop.ts
|
|
5
|
+
* (_currentResolve). handleAgentEnd is a thin compatibility wrapper around
|
|
6
|
+
* resolveAgentEnd().
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import test from "node:test";
|
|
@@ -14,40 +14,43 @@ import { fileURLToPath } from "node:url";
|
|
|
14
14
|
|
|
15
15
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
16
16
|
const AUTO_TS_PATH = join(__dirname, "..", "auto.ts");
|
|
17
|
+
const AUTO_LOOP_TS_PATH = join(__dirname, "..", "auto-loop.ts");
|
|
17
18
|
const SESSION_TS_PATH = join(__dirname, "..", "auto", "session.ts");
|
|
18
19
|
|
|
19
20
|
function getAutoTsSource(): string {
|
|
20
21
|
return readFileSync(AUTO_TS_PATH, "utf-8");
|
|
21
22
|
}
|
|
22
23
|
|
|
24
|
+
function getAutoLoopTsSource(): string {
|
|
25
|
+
return readFileSync(AUTO_LOOP_TS_PATH, "utf-8");
|
|
26
|
+
}
|
|
27
|
+
|
|
23
28
|
function getSessionTsSource(): string {
|
|
24
29
|
return readFileSync(SESSION_TS_PATH, "utf-8");
|
|
25
30
|
}
|
|
26
31
|
|
|
27
|
-
test("
|
|
28
|
-
const source =
|
|
32
|
+
test("auto-loop.ts declares _currentResolve for per-unit one-shot promises", () => {
|
|
33
|
+
const source = getAutoLoopTsSource();
|
|
29
34
|
assert.ok(
|
|
30
|
-
source.includes("
|
|
31
|
-
"
|
|
35
|
+
source.includes("_currentResolve"),
|
|
36
|
+
"auto-loop.ts must declare _currentResolve for the per-unit resolve function",
|
|
32
37
|
);
|
|
33
38
|
assert.ok(
|
|
34
|
-
source.includes("
|
|
35
|
-
"
|
|
39
|
+
source.includes("_sessionSwitchInFlight"),
|
|
40
|
+
"auto-loop.ts must declare _sessionSwitchInFlight guard",
|
|
36
41
|
);
|
|
37
42
|
});
|
|
38
43
|
|
|
39
|
-
test("AutoSession
|
|
44
|
+
test("AutoSession no longer holds promise state (moved to auto-loop.ts module scope)", () => {
|
|
40
45
|
const source = getSessionTsSource();
|
|
41
|
-
|
|
42
|
-
assert.ok(resetIdx > -1, "AutoSession must have a reset() method");
|
|
43
|
-
const resetBlock = source.slice(resetIdx, resetIdx + 4000);
|
|
46
|
+
// Properties should NOT exist as class fields
|
|
44
47
|
assert.ok(
|
|
45
|
-
|
|
46
|
-
"
|
|
48
|
+
!source.includes("pendingResolve:"),
|
|
49
|
+
"AutoSession must not declare pendingResolve (moved to auto-loop.ts)",
|
|
47
50
|
);
|
|
48
51
|
assert.ok(
|
|
49
|
-
|
|
50
|
-
"
|
|
52
|
+
!source.includes("pendingAgentEndQueue:"),
|
|
53
|
+
"AutoSession must not declare pendingAgentEndQueue (removed — events are dropped)",
|
|
51
54
|
);
|
|
52
55
|
});
|
|
53
56
|
|
|
@@ -37,9 +37,6 @@ function makeMockSession(opts?: {
|
|
|
37
37
|
const session = {
|
|
38
38
|
active: true,
|
|
39
39
|
verbose: false,
|
|
40
|
-
sessionSwitchInFlight: false,
|
|
41
|
-
pendingResolve: null,
|
|
42
|
-
pendingAgentEndQueue: [],
|
|
43
40
|
cmdCtx: {
|
|
44
41
|
newSession: () => {
|
|
45
42
|
opts?.onNewSessionStart?.(session);
|
|
@@ -96,7 +93,6 @@ test("resolveAgentEnd resolves a pending runUnit promise", async () => {
|
|
|
96
93
|
const ctx = makeMockCtx();
|
|
97
94
|
const pi = makeMockPi();
|
|
98
95
|
const s = makeMockSession();
|
|
99
|
-
_setActiveSession(s);
|
|
100
96
|
const event = makeEvent();
|
|
101
97
|
|
|
102
98
|
// Start runUnit — it will create the promise and send a message,
|
|
@@ -108,7 +104,6 @@ test("resolveAgentEnd resolves a pending runUnit promise", async () => {
|
|
|
108
104
|
"task",
|
|
109
105
|
"T01",
|
|
110
106
|
"do stuff",
|
|
111
|
-
undefined,
|
|
112
107
|
);
|
|
113
108
|
|
|
114
109
|
// Give the microtask queue a tick so runUnit reaches the await
|
|
@@ -122,44 +117,35 @@ test("resolveAgentEnd resolves a pending runUnit promise", async () => {
|
|
|
122
117
|
assert.deepEqual(result.event, event);
|
|
123
118
|
});
|
|
124
119
|
|
|
125
|
-
test("resolveAgentEnd
|
|
120
|
+
test("resolveAgentEnd drops event when no promise is pending", () => {
|
|
126
121
|
_resetPendingResolve();
|
|
127
|
-
const s = makeMockSession();
|
|
128
|
-
_setActiveSession(s);
|
|
129
122
|
|
|
130
|
-
// Should not throw —
|
|
123
|
+
// Should not throw — event is dropped (logged as warning)
|
|
131
124
|
assert.doesNotThrow(() => {
|
|
132
125
|
resolveAgentEnd(makeEvent());
|
|
133
126
|
});
|
|
134
|
-
assert.equal(s.pendingAgentEndQueue.length, 1, "event should be queued");
|
|
135
127
|
});
|
|
136
128
|
|
|
137
|
-
test("double resolveAgentEnd only resolves once (second is
|
|
129
|
+
test("double resolveAgentEnd only resolves once (second is dropped)", async () => {
|
|
138
130
|
_resetPendingResolve();
|
|
139
131
|
|
|
140
132
|
const ctx = makeMockCtx();
|
|
141
133
|
const pi = makeMockPi();
|
|
142
134
|
const s = makeMockSession();
|
|
143
|
-
_setActiveSession(s);
|
|
144
135
|
const event1 = makeEvent([{ id: 1 }]);
|
|
145
136
|
const event2 = makeEvent([{ id: 2 }]);
|
|
146
137
|
|
|
147
|
-
const resultPromise = runUnit(ctx, pi, s, "task", "T01", "prompt"
|
|
138
|
+
const resultPromise = runUnit(ctx, pi, s, "task", "T01", "prompt");
|
|
148
139
|
|
|
149
140
|
await new Promise((r) => setTimeout(r, 10));
|
|
150
141
|
|
|
151
142
|
// First resolve — should work
|
|
152
143
|
resolveAgentEnd(event1);
|
|
153
144
|
|
|
154
|
-
// Second resolve — should be
|
|
145
|
+
// Second resolve — should be dropped (no pending resolver)
|
|
155
146
|
assert.doesNotThrow(() => {
|
|
156
147
|
resolveAgentEnd(event2);
|
|
157
148
|
});
|
|
158
|
-
assert.equal(
|
|
159
|
-
s.pendingAgentEndQueue.length,
|
|
160
|
-
1,
|
|
161
|
-
"second event should be queued",
|
|
162
|
-
);
|
|
163
149
|
|
|
164
150
|
const result = await resultPromise;
|
|
165
151
|
assert.equal(result.status, "completed");
|
|
@@ -174,7 +160,7 @@ test("runUnit returns cancelled when session creation fails", async () => {
|
|
|
174
160
|
const pi = makeMockPi();
|
|
175
161
|
const s = makeMockSession({ newSessionThrows: "connection refused" });
|
|
176
162
|
|
|
177
|
-
const result = await runUnit(ctx, pi, s, "task", "T01", "prompt"
|
|
163
|
+
const result = await runUnit(ctx, pi, s, "task", "T01", "prompt");
|
|
178
164
|
|
|
179
165
|
assert.equal(result.status, "cancelled");
|
|
180
166
|
assert.equal(result.event, undefined);
|
|
@@ -190,7 +176,7 @@ test("runUnit returns cancelled when session creation times out", async () => {
|
|
|
190
176
|
// Session returns cancelled: true (simulates the timeout race outcome)
|
|
191
177
|
const s = makeMockSession({ newSessionResult: { cancelled: true } });
|
|
192
178
|
|
|
193
|
-
const result = await runUnit(ctx, pi, s, "task", "T01", "prompt"
|
|
179
|
+
const result = await runUnit(ctx, pi, s, "task", "T01", "prompt");
|
|
194
180
|
|
|
195
181
|
assert.equal(result.status, "cancelled");
|
|
196
182
|
assert.equal(result.event, undefined);
|
|
@@ -205,35 +191,31 @@ test("runUnit returns cancelled when s.active is false before sendMessage", asyn
|
|
|
205
191
|
const s = makeMockSession();
|
|
206
192
|
s.active = false;
|
|
207
193
|
|
|
208
|
-
const result = await runUnit(ctx, pi, s, "task", "T01", "prompt"
|
|
194
|
+
const result = await runUnit(ctx, pi, s, "task", "T01", "prompt");
|
|
209
195
|
|
|
210
196
|
assert.equal(result.status, "cancelled");
|
|
211
197
|
assert.equal(pi.calls.length, 0);
|
|
212
198
|
});
|
|
213
199
|
|
|
214
|
-
test("runUnit only arms
|
|
200
|
+
test("runUnit only arms resolve after newSession completes", async () => {
|
|
215
201
|
_resetPendingResolve();
|
|
216
202
|
|
|
217
203
|
let sawSwitchFlag = false;
|
|
218
|
-
let sawPendingResolve: unknown = "unset";
|
|
219
204
|
|
|
220
205
|
const ctx = makeMockCtx();
|
|
221
206
|
const pi = makeMockPi();
|
|
222
207
|
const s = makeMockSession({
|
|
223
208
|
newSessionDelayMs: 20,
|
|
224
|
-
onNewSessionStart: (
|
|
225
|
-
sawSwitchFlag =
|
|
226
|
-
sawPendingResolve = session.pendingResolve;
|
|
209
|
+
onNewSessionStart: () => {
|
|
210
|
+
sawSwitchFlag = isSessionSwitchInFlight();
|
|
227
211
|
},
|
|
228
212
|
});
|
|
229
|
-
_setActiveSession(s);
|
|
230
213
|
|
|
231
|
-
const resultPromise = runUnit(ctx, pi, s, "task", "T01", "prompt"
|
|
214
|
+
const resultPromise = runUnit(ctx, pi, s, "task", "T01", "prompt");
|
|
232
215
|
|
|
233
216
|
await new Promise((r) => setTimeout(r, 30));
|
|
234
217
|
|
|
235
218
|
assert.equal(sawSwitchFlag, true, "session switch guard should be active during newSession");
|
|
236
|
-
assert.equal(sawPendingResolve, null, "pendingResolve should not be armed before newSession completes");
|
|
237
219
|
assert.equal(isSessionSwitchInFlight(), false, "session switch guard should clear after newSession settles");
|
|
238
220
|
|
|
239
221
|
resolveAgentEnd(makeEvent());
|
|
@@ -275,24 +257,23 @@ test("auto-loop.ts contains a while keyword", () => {
|
|
|
275
257
|
);
|
|
276
258
|
});
|
|
277
259
|
|
|
278
|
-
test("auto-loop.ts one-shot pattern:
|
|
260
|
+
test("auto-loop.ts one-shot pattern: _currentResolve is nulled before calling resolver", () => {
|
|
279
261
|
const src = readFileSync(
|
|
280
262
|
resolve(import.meta.dirname, "..", "auto-loop.ts"),
|
|
281
263
|
"utf-8",
|
|
282
264
|
);
|
|
283
265
|
// The one-shot pattern requires: save ref, null the variable, then call
|
|
284
|
-
// Look for the pattern: s.pendingResolve = null appearing before r(
|
|
285
266
|
const resolveBlock = src.slice(
|
|
286
267
|
src.indexOf("export function resolveAgentEnd"),
|
|
287
268
|
src.indexOf("export function resolveAgentEnd") + 600,
|
|
288
269
|
);
|
|
289
|
-
const nullIdx = resolveBlock.indexOf("
|
|
270
|
+
const nullIdx = resolveBlock.indexOf("_currentResolve = null");
|
|
290
271
|
const callIdx = resolveBlock.indexOf("r({");
|
|
291
|
-
assert.ok(nullIdx > 0, "should null
|
|
272
|
+
assert.ok(nullIdx > 0, "should null _currentResolve in resolveAgentEnd");
|
|
292
273
|
assert.ok(callIdx > 0, "should call resolver in resolveAgentEnd");
|
|
293
274
|
assert.ok(
|
|
294
275
|
nullIdx < callIdx,
|
|
295
|
-
"
|
|
276
|
+
"_currentResolve should be nulled before calling the resolver (one-shot)",
|
|
296
277
|
);
|
|
297
278
|
});
|
|
298
279
|
|
|
@@ -462,8 +443,6 @@ function makeLoopSession(overrides?: Partial<Record<string, unknown>>) {
|
|
|
462
443
|
pendingQuickTasks: [],
|
|
463
444
|
sidecarQueue: [],
|
|
464
445
|
autoModeStartModel: null,
|
|
465
|
-
pendingResolve: null,
|
|
466
|
-
pendingAgentEndQueue: [],
|
|
467
446
|
unitDispatchCount: new Map<string, number>(),
|
|
468
447
|
unitLifetimeDispatches: new Map<string, number>(),
|
|
469
448
|
unitRecoveryCount: new Map<string, number>(),
|
|
@@ -100,6 +100,99 @@ test("buildCmuxStatusLabel and progress prefer deepest active unit", () => {
|
|
|
100
100
|
assert.deepEqual(buildCmuxProgress(state), { value: 0.4, label: "2/5 tasks" });
|
|
101
101
|
});
|
|
102
102
|
|
|
103
|
+
describe("createGridLayout", () => {
|
|
104
|
+
// Create a mock CmuxClient that tracks createSplitFrom calls
|
|
105
|
+
function makeMockClient() {
|
|
106
|
+
let nextId = 1;
|
|
107
|
+
const calls: Array<{ source: string | undefined; direction: string }> = [];
|
|
108
|
+
|
|
109
|
+
const client = {
|
|
110
|
+
calls,
|
|
111
|
+
async createGridLayout(count: number) {
|
|
112
|
+
// Simulate the grid layout logic with a fake client
|
|
113
|
+
if (count <= 0) return [];
|
|
114
|
+
const surfaces: string[] = [];
|
|
115
|
+
|
|
116
|
+
const createSplitFrom = async (source: string | undefined, direction: string) => {
|
|
117
|
+
calls.push({ source, direction });
|
|
118
|
+
return `surface-${nextId++}`;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const rightCol = await createSplitFrom("gsd-surface", "right");
|
|
122
|
+
surfaces.push(rightCol);
|
|
123
|
+
if (count === 1) return surfaces;
|
|
124
|
+
|
|
125
|
+
const bottomRight = await createSplitFrom(rightCol, "down");
|
|
126
|
+
surfaces.push(bottomRight);
|
|
127
|
+
if (count === 2) return surfaces;
|
|
128
|
+
|
|
129
|
+
const bottomLeft = await createSplitFrom("gsd-surface", "down");
|
|
130
|
+
surfaces.push(bottomLeft);
|
|
131
|
+
if (count === 3) return surfaces;
|
|
132
|
+
|
|
133
|
+
let lastSurface = bottomRight;
|
|
134
|
+
for (let i = 3; i < count; i++) {
|
|
135
|
+
const next = await createSplitFrom(lastSurface, "down");
|
|
136
|
+
surfaces.push(next);
|
|
137
|
+
lastSurface = next;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return surfaces;
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
return client;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
test("1 agent creates single right split", async () => {
|
|
147
|
+
const mock = makeMockClient();
|
|
148
|
+
const surfaces = await mock.createGridLayout(1);
|
|
149
|
+
assert.equal(surfaces.length, 1);
|
|
150
|
+
assert.deepEqual(mock.calls, [
|
|
151
|
+
{ source: "gsd-surface", direction: "right" },
|
|
152
|
+
]);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("2 agents creates right column then splits it down", async () => {
|
|
156
|
+
const mock = makeMockClient();
|
|
157
|
+
const surfaces = await mock.createGridLayout(2);
|
|
158
|
+
assert.equal(surfaces.length, 2);
|
|
159
|
+
assert.deepEqual(mock.calls, [
|
|
160
|
+
{ source: "gsd-surface", direction: "right" },
|
|
161
|
+
{ source: "surface-1", direction: "down" },
|
|
162
|
+
]);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("3 agents creates 2x2 grid (gsd + 3 agent surfaces)", async () => {
|
|
166
|
+
const mock = makeMockClient();
|
|
167
|
+
const surfaces = await mock.createGridLayout(3);
|
|
168
|
+
assert.equal(surfaces.length, 3);
|
|
169
|
+
assert.deepEqual(mock.calls, [
|
|
170
|
+
{ source: "gsd-surface", direction: "right" },
|
|
171
|
+
{ source: "surface-1", direction: "down" },
|
|
172
|
+
{ source: "gsd-surface", direction: "down" },
|
|
173
|
+
]);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("4 agents creates 2x2 grid with extra split", async () => {
|
|
177
|
+
const mock = makeMockClient();
|
|
178
|
+
const surfaces = await mock.createGridLayout(4);
|
|
179
|
+
assert.equal(surfaces.length, 4);
|
|
180
|
+
assert.deepEqual(mock.calls, [
|
|
181
|
+
{ source: "gsd-surface", direction: "right" },
|
|
182
|
+
{ source: "surface-1", direction: "down" },
|
|
183
|
+
{ source: "gsd-surface", direction: "down" },
|
|
184
|
+
{ source: "surface-2", direction: "down" },
|
|
185
|
+
]);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test("0 agents returns empty", async () => {
|
|
189
|
+
const mock = makeMockClient();
|
|
190
|
+
const surfaces = await mock.createGridLayout(0);
|
|
191
|
+
assert.equal(surfaces.length, 0);
|
|
192
|
+
assert.equal(mock.calls.length, 0);
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
103
196
|
describe("cmux extension discovery opt-out", () => {
|
|
104
197
|
test("cmux directory has package.json with pi manifest to prevent auto-discovery as extension", () => {
|
|
105
198
|
const cmuxDir = path.resolve(
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { mkdtempSync, mkdirSync, rmSync, writeFileSync, existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
|
|
5
|
+
import { runGSDDoctor } from "../doctor.js";
|
|
6
|
+
import { formatDoctorReportJson } from "../doctor-format.js";
|
|
7
|
+
import { createTestContext } from "./test-helpers.ts";
|
|
8
|
+
|
|
9
|
+
const { assertEq, assertTrue, assertMatch, report } = createTestContext();
|
|
10
|
+
|
|
11
|
+
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
function makeBase(): { base: string; gsd: string; mDir: string } {
|
|
14
|
+
const base = mkdtempSync(join(tmpdir(), "gsd-doctor-enh-"));
|
|
15
|
+
const gsd = join(base, ".gsd");
|
|
16
|
+
const mDir = join(gsd, "milestones", "M001");
|
|
17
|
+
mkdirSync(join(mDir, "slices"), { recursive: true });
|
|
18
|
+
return { base, gsd, mDir };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function writeRoadmap(mDir: string, content: string): void {
|
|
22
|
+
writeFileSync(join(mDir, "M001-ROADMAP.md"), content);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function writeSlice(mDir: string, sliceId: string, planContent: string): string {
|
|
26
|
+
const sDir = join(mDir, "slices", sliceId);
|
|
27
|
+
const tDir = join(sDir, "tasks");
|
|
28
|
+
mkdirSync(tDir, { recursive: true });
|
|
29
|
+
writeFileSync(join(sDir, `${sliceId}-PLAN.md`), planContent);
|
|
30
|
+
return sDir;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function main(): Promise<void> {
|
|
34
|
+
// ── 1. Circular dependency detection ──────────────────────────────────────
|
|
35
|
+
console.log("\n=== circular dependency detection ===");
|
|
36
|
+
{
|
|
37
|
+
const { base, mDir } = makeBase();
|
|
38
|
+
writeRoadmap(mDir, `# M001: Circular Test\n\n## Slices\n- [ ] **S01: Slice A** \`risk:low\` \`depends:[S02]\`\n > After this: done\n- [ ] **S02: Slice B** \`risk:low\` \`depends:[S01]\`\n > After this: done\n`);
|
|
39
|
+
writeSlice(mDir, "S01", "# S01: Slice A\n\n**Goal:** A\n**Demo:** A\n\n## Tasks\n- [ ] **T01: Task** `est:10m`\n Pending.\n");
|
|
40
|
+
writeSlice(mDir, "S02", "# S02: Slice B\n\n**Goal:** B\n**Demo:** B\n\n## Tasks\n- [ ] **T01: Task** `est:10m`\n Pending.\n");
|
|
41
|
+
|
|
42
|
+
const result = await runGSDDoctor(base, { fix: false });
|
|
43
|
+
assertTrue(
|
|
44
|
+
result.issues.some(i => i.code === "circular_slice_dependency"),
|
|
45
|
+
"detects circular dependency S01 → S02 → S01",
|
|
46
|
+
);
|
|
47
|
+
rmSync(base, { recursive: true, force: true });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ── 2. Duplicate task IDs ──────────────────────────────────────────────────
|
|
51
|
+
console.log("\n=== duplicate task IDs ===");
|
|
52
|
+
{
|
|
53
|
+
const { base, mDir } = makeBase();
|
|
54
|
+
writeRoadmap(mDir, `# M001: Dup Test\n\n## Slices\n- [ ] **S01: Slice** \`risk:low\` \`depends:[]\`\n > After this: done\n`);
|
|
55
|
+
writeSlice(mDir, "S01", "# S01: Slice\n\n**Goal:** G\n**Demo:** D\n\n## Tasks\n- [ ] **T01: First** `est:10m`\n Task one.\n- [ ] **T01: Duplicate** `est:10m`\n Task dup.\n");
|
|
56
|
+
|
|
57
|
+
const result = await runGSDDoctor(base, { fix: false });
|
|
58
|
+
assertTrue(
|
|
59
|
+
result.issues.some(i => i.code === "duplicate_task_id"),
|
|
60
|
+
"detects duplicate task ID T01",
|
|
61
|
+
);
|
|
62
|
+
rmSync(base, { recursive: true, force: true });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ── 3. Orphaned slice directory ──────────────────────────────────────────
|
|
66
|
+
console.log("\n=== orphaned slice directory ===");
|
|
67
|
+
{
|
|
68
|
+
const { base, mDir } = makeBase();
|
|
69
|
+
writeRoadmap(mDir, `# M001: Orphan Test\n\n## Slices\n- [ ] **S01: Slice** \`risk:low\` \`depends:[]\`\n > After this: done\n`);
|
|
70
|
+
writeSlice(mDir, "S01", "# S01: Slice\n\n**Goal:** G\n**Demo:** D\n\n## Tasks\n- [ ] **T01: Task** `est:10m`\n Pending.\n");
|
|
71
|
+
// Create an extra slice directory not in roadmap
|
|
72
|
+
mkdirSync(join(mDir, "slices", "S99"), { recursive: true });
|
|
73
|
+
|
|
74
|
+
const result = await runGSDDoctor(base, { fix: false });
|
|
75
|
+
assertTrue(
|
|
76
|
+
result.issues.some(i => i.code === "orphaned_slice_directory" && i.message.includes("S99")),
|
|
77
|
+
"detects orphaned slice directory S99",
|
|
78
|
+
);
|
|
79
|
+
rmSync(base, { recursive: true, force: true });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ── 4. Task file not in plan ───────────────────────────────────────────────
|
|
83
|
+
console.log("\n=== task file not in plan ===");
|
|
84
|
+
{
|
|
85
|
+
const { base, mDir } = makeBase();
|
|
86
|
+
writeRoadmap(mDir, `# M001: Extra Task Test\n\n## Slices\n- [ ] **S01: Slice** \`risk:low\` \`depends:[]\`\n > After this: done\n`);
|
|
87
|
+
const sDir = writeSlice(mDir, "S01", "# S01: Slice\n\n**Goal:** G\n**Demo:** D\n\n## Tasks\n- [x] **T01: Task** `est:10m`\n Done.\n");
|
|
88
|
+
// T01 summary (matches plan)
|
|
89
|
+
writeFileSync(join(sDir, "tasks", "T01-SUMMARY.md"), "---\nstatus: done\n---\n# T01\nDone.\n");
|
|
90
|
+
// T99 summary (NOT in plan)
|
|
91
|
+
writeFileSync(join(sDir, "tasks", "T99-SUMMARY.md"), "---\nstatus: done\n---\n# T99\nExtra.\n");
|
|
92
|
+
|
|
93
|
+
const result = await runGSDDoctor(base, { fix: false });
|
|
94
|
+
assertTrue(
|
|
95
|
+
result.issues.some(i => i.code === "task_file_not_in_plan" && i.message.includes("T99")),
|
|
96
|
+
"detects task summary T99 not in plan",
|
|
97
|
+
);
|
|
98
|
+
rmSync(base, { recursive: true, force: true });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ── 5. Stale REPLAN file ────────────────────────────────────────────────────
|
|
102
|
+
console.log("\n=== stale REPLAN detection ===");
|
|
103
|
+
{
|
|
104
|
+
const { base, mDir } = makeBase();
|
|
105
|
+
writeRoadmap(mDir, `# M001: Replan Test\n\n## Slices\n- [ ] **S01: Slice** \`risk:low\` \`depends:[]\`\n > After this: done\n`);
|
|
106
|
+
const sDir = writeSlice(mDir, "S01", "# S01: Slice\n\n**Goal:** G\n**Demo:** D\n\n## Tasks\n- [x] **T01: Task** `est:10m`\n Done.\n");
|
|
107
|
+
writeFileSync(join(sDir, "tasks", "T01-SUMMARY.md"), "---\nstatus: done\ncompleted_at: 2026-01-01T00:00:00Z\n---\n# T01\nDone.\n");
|
|
108
|
+
// Add a REPLAN file even though all tasks are done
|
|
109
|
+
writeFileSync(join(sDir, "S01-REPLAN.md"), "# S01 REPLAN\nSomething changed.\n");
|
|
110
|
+
|
|
111
|
+
const result = await runGSDDoctor(base, { fix: false });
|
|
112
|
+
assertTrue(
|
|
113
|
+
result.issues.some(i => i.code === "stale_replan_file"),
|
|
114
|
+
"detects stale REPLAN when all tasks are done",
|
|
115
|
+
);
|
|
116
|
+
rmSync(base, { recursive: true, force: true });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ── 6. Metrics ledger corrupt ───────────────────────────────────────────────
|
|
120
|
+
console.log("\n=== metrics ledger corrupt ===");
|
|
121
|
+
{
|
|
122
|
+
const { base, gsd, mDir } = makeBase();
|
|
123
|
+
writeRoadmap(mDir, `# M001: Metrics Test\n\n## Slices\n- [ ] **S01: Slice** \`risk:low\` \`depends:[]\`\n > After this: done\n`);
|
|
124
|
+
writeSlice(mDir, "S01", "# S01: Slice\n\n**Goal:** G\n**Demo:** D\n\n## Tasks\n- [ ] **T01: Task** `est:10m`\n Pending.\n");
|
|
125
|
+
// Write invalid metrics.json
|
|
126
|
+
writeFileSync(join(gsd, "metrics.json"), '{"version":2,"data":[]}');
|
|
127
|
+
|
|
128
|
+
const result = await runGSDDoctor(base, { fix: false });
|
|
129
|
+
assertTrue(
|
|
130
|
+
result.issues.some(i => i.code === "metrics_ledger_corrupt"),
|
|
131
|
+
"detects corrupt metrics ledger (version != 1)",
|
|
132
|
+
);
|
|
133
|
+
rmSync(base, { recursive: true, force: true });
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ── 7. Large planning file ──────────────────────────────────────────────────
|
|
137
|
+
console.log("\n=== large planning file ===");
|
|
138
|
+
{
|
|
139
|
+
const { base, mDir } = makeBase();
|
|
140
|
+
writeRoadmap(mDir, `# M001: Large File Test\n\n## Slices\n- [ ] **S01: Slice** \`risk:low\` \`depends:[]\`\n > After this: done\n`);
|
|
141
|
+
const sDir = writeSlice(mDir, "S01", "# S01: Slice\n\n**Goal:** G\n**Demo:** D\n\n## Tasks\n- [ ] **T01: Task** `est:10m`\n Pending.\n");
|
|
142
|
+
// Write a 101KB .md file
|
|
143
|
+
const bigContent = "# Big File\n" + "x".repeat(101 * 1024);
|
|
144
|
+
writeFileSync(join(sDir, "BIGFILE.md"), bigContent);
|
|
145
|
+
|
|
146
|
+
const result = await runGSDDoctor(base, { fix: false });
|
|
147
|
+
assertTrue(
|
|
148
|
+
result.issues.some(i => i.code === "large_planning_file"),
|
|
149
|
+
"detects large planning file over 100KB",
|
|
150
|
+
);
|
|
151
|
+
rmSync(base, { recursive: true, force: true });
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ── 8. Future timestamp ─────────────────────────────────────────────────────
|
|
155
|
+
console.log("\n=== future timestamp ===");
|
|
156
|
+
{
|
|
157
|
+
const { base, mDir } = makeBase();
|
|
158
|
+
writeRoadmap(mDir, `# M001: Timestamp Test\n\n## Slices\n- [ ] **S01: Slice** \`risk:low\` \`depends:[]\`\n > After this: done\n`);
|
|
159
|
+
const sDir = writeSlice(mDir, "S01", "# S01: Slice\n\n**Goal:** G\n**Demo:** D\n\n## Tasks\n- [x] **T01: Task** `est:10m`\n Done.\n");
|
|
160
|
+
// completed_at is 2 days in the future
|
|
161
|
+
const futureDate = new Date(Date.now() + 2 * 24 * 60 * 60 * 1000).toISOString();
|
|
162
|
+
writeFileSync(
|
|
163
|
+
join(sDir, "tasks", "T01-SUMMARY.md"),
|
|
164
|
+
`---\nstatus: done\ncompleted_at: ${futureDate}\n---\n# T01\nDone.\n`,
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
const result = await runGSDDoctor(base, { fix: false });
|
|
168
|
+
assertTrue(
|
|
169
|
+
result.issues.some(i => i.code === "future_timestamp"),
|
|
170
|
+
"detects future completed_at timestamp",
|
|
171
|
+
);
|
|
172
|
+
rmSync(base, { recursive: true, force: true });
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ── 9. JSON output format ───────────────────────────────────────────────────
|
|
176
|
+
console.log("\n=== JSON output format ===");
|
|
177
|
+
{
|
|
178
|
+
const { base, mDir } = makeBase();
|
|
179
|
+
writeRoadmap(mDir, `# M001: JSON Test\n\n## Slices\n- [ ] **S01: Slice** \`risk:low\` \`depends:[]\`\n > After this: done\n`);
|
|
180
|
+
writeSlice(mDir, "S01", "# S01: Slice\n\n**Goal:** G\n**Demo:** D\n\n## Tasks\n- [ ] **T01: Task** `est:10m`\n Pending.\n");
|
|
181
|
+
|
|
182
|
+
const result = await runGSDDoctor(base, { fix: false });
|
|
183
|
+
const json = formatDoctorReportJson(result);
|
|
184
|
+
|
|
185
|
+
let parsed: unknown;
|
|
186
|
+
try {
|
|
187
|
+
parsed = JSON.parse(json);
|
|
188
|
+
} catch {
|
|
189
|
+
parsed = null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
assertTrue(parsed !== null, "formatDoctorReportJson produces valid JSON");
|
|
193
|
+
assertTrue(typeof (parsed as Record<string, unknown>)?.ok === "boolean", "JSON has ok field");
|
|
194
|
+
assertTrue(Array.isArray((parsed as Record<string, unknown>)?.issues), "JSON has issues array");
|
|
195
|
+
assertTrue(Array.isArray((parsed as Record<string, unknown>)?.fixesApplied), "JSON has fixesApplied array");
|
|
196
|
+
assertTrue(typeof (parsed as Record<string, unknown>)?.generatedAt === "string", "JSON has generatedAt field");
|
|
197
|
+
assertTrue(typeof (parsed as Record<string, unknown>)?.summary === "object", "JSON has summary object");
|
|
198
|
+
|
|
199
|
+
rmSync(base, { recursive: true, force: true });
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ── 10. Dry-run mode ────────────────────────────────────────────────────────
|
|
203
|
+
console.log("\n=== dry-run mode ===");
|
|
204
|
+
{
|
|
205
|
+
const { base, mDir } = makeBase();
|
|
206
|
+
writeRoadmap(mDir, `# M001: Dry Run Test\n\n## Slices\n- [ ] **S01: Slice** \`risk:low\` \`depends:[]\`\n > After this: done\n`);
|
|
207
|
+
const sDir = writeSlice(mDir, "S01", "# S01: Slice\n\n**Goal:** G\n**Demo:** D\n\n## Tasks\n- [x] **T01: Task** `est:10m`\n Done.\n");
|
|
208
|
+
|
|
209
|
+
const result = await runGSDDoctor(base, { fix: true, dryRun: true });
|
|
210
|
+
// In dry-run mode, no actual files should be created
|
|
211
|
+
assertTrue(!existsSync(join(sDir, "S01-SUMMARY.md")), "dry-run does not create slice summary");
|
|
212
|
+
assertTrue(
|
|
213
|
+
result.fixesApplied.some(f => f.startsWith("[dry-run]")),
|
|
214
|
+
"dry-run mode reports would-fix entries",
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
rmSync(base, { recursive: true, force: true });
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ── 11. Per-check timing ─────────────────────────────────────────────────────
|
|
221
|
+
console.log("\n=== per-check timing ===");
|
|
222
|
+
{
|
|
223
|
+
const { base, mDir } = makeBase();
|
|
224
|
+
writeRoadmap(mDir, `# M001: Timing Test\n\n## Slices\n- [ ] **S01: Slice** \`risk:low\` \`depends:[]\`\n > After this: done\n`);
|
|
225
|
+
writeSlice(mDir, "S01", "# S01: Slice\n\n**Goal:** G\n**Demo:** D\n\n## Tasks\n- [ ] **T01: Task** `est:10m`\n Pending.\n");
|
|
226
|
+
|
|
227
|
+
const result = await runGSDDoctor(base, { fix: false });
|
|
228
|
+
assertTrue(result.timing !== undefined, "report includes timing");
|
|
229
|
+
assertTrue(typeof result.timing?.git === "number", "timing.git is a number");
|
|
230
|
+
assertTrue(typeof result.timing?.runtime === "number", "timing.runtime is a number");
|
|
231
|
+
assertTrue(typeof result.timing?.environment === "number", "timing.environment is a number");
|
|
232
|
+
assertTrue(typeof result.timing?.gsdState === "number", "timing.gsdState is a number");
|
|
233
|
+
|
|
234
|
+
rmSync(base, { recursive: true, force: true });
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ── 12. Doctor history ───────────────────────────────────────────────────────
|
|
238
|
+
console.log("\n=== doctor history ===");
|
|
239
|
+
{
|
|
240
|
+
const { base, gsd, mDir } = makeBase();
|
|
241
|
+
writeRoadmap(mDir, `# M001: History Test\n\n## Slices\n- [ ] **S01: Slice** \`risk:low\` \`depends:[]\`\n > After this: done\n`);
|
|
242
|
+
writeSlice(mDir, "S01", "# S01: Slice\n\n**Goal:** G\n**Demo:** D\n\n## Tasks\n- [ ] **T01: Task** `est:10m`\n Pending.\n");
|
|
243
|
+
|
|
244
|
+
await runGSDDoctor(base, { fix: false });
|
|
245
|
+
|
|
246
|
+
const historyPath = join(gsd, "doctor-history.jsonl");
|
|
247
|
+
assertTrue(existsSync(historyPath), "doctor-history.jsonl is created after run");
|
|
248
|
+
|
|
249
|
+
const { readDoctorHistory } = await import("../doctor.js");
|
|
250
|
+
const history = await readDoctorHistory(base);
|
|
251
|
+
assertTrue(history.length >= 1, "history has at least one entry");
|
|
252
|
+
assertTrue(typeof history[0]?.ts === "string", "history entry has ts field");
|
|
253
|
+
assertTrue(typeof history[0]?.ok === "boolean", "history entry has ok field");
|
|
254
|
+
assertTrue(typeof history[0]?.errors === "number", "history entry has errors count");
|
|
255
|
+
assertTrue(Array.isArray(history[0]?.codes), "history entry has codes array");
|
|
256
|
+
|
|
257
|
+
rmSync(base, { recursive: true, force: true });
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
report();
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
main().catch(err => {
|
|
264
|
+
console.error(err);
|
|
265
|
+
process.exit(1);
|
|
266
|
+
});
|