gsd-pi 2.77.0-dev.1d17f366c → 2.77.0-dev.58d3d4d6c
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/resources/extensions/gsd/auto/session.js +6 -0
- package/dist/resources/extensions/gsd/auto-post-unit.js +79 -0
- package/dist/resources/extensions/gsd/auto-prompts.js +48 -7
- package/dist/resources/extensions/gsd/auto-start.js +62 -3
- package/dist/resources/extensions/gsd/auto.js +34 -0
- package/dist/resources/extensions/gsd/context-store.js +23 -7
- package/dist/resources/extensions/gsd/forensics.js +106 -0
- package/dist/resources/extensions/gsd/preferences-validation.js +23 -0
- package/dist/resources/extensions/gsd/prompt-cache-optimizer.js +4 -0
- package/dist/resources/extensions/gsd/slice-cadence.js +238 -0
- package/dist/resources/extensions/gsd/tools/validate-milestone.js +7 -2
- package/dist/resources/extensions/gsd/worktree-manager.js +51 -0
- package/dist/resources/extensions/gsd/worktree-resolver.js +86 -7
- package/dist/resources/extensions/gsd/worktree-telemetry.js +198 -0
- package/dist/tsconfig.extensions.tsbuildinfo +1 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +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/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/middleware-manifest.json +5 -5
- 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/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-execution.test.js +36 -5
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-execution.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +30 -12
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/packages/pi-coding-agent/src/modes/interactive/components/__tests__/tool-execution.test.ts +49 -3
- package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +48 -9
- package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
- package/src/resources/extensions/gsd/auto/session.ts +7 -0
- package/src/resources/extensions/gsd/auto-post-unit.ts +81 -0
- package/src/resources/extensions/gsd/auto-prompts.ts +59 -7
- package/src/resources/extensions/gsd/auto-start.ts +64 -2
- package/src/resources/extensions/gsd/auto.ts +37 -0
- package/src/resources/extensions/gsd/context-store.ts +25 -8
- package/src/resources/extensions/gsd/forensics.ts +118 -1
- package/src/resources/extensions/gsd/git-service.ts +16 -0
- package/src/resources/extensions/gsd/journal.ts +11 -1
- package/src/resources/extensions/gsd/preferences-validation.ts +21 -0
- package/src/resources/extensions/gsd/prompt-cache-optimizer.ts +4 -0
- package/src/resources/extensions/gsd/slice-cadence.ts +299 -0
- package/src/resources/extensions/gsd/tests/artifacts-table-preserved-on-cache-invalidate.test.ts +2 -1
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +5 -8
- package/src/resources/extensions/gsd/tests/auto-remediate-slice-status.test.ts +4 -1
- package/src/resources/extensions/gsd/tests/auto-retry-mcp-churn-fixes.test.ts +12 -9
- package/src/resources/extensions/gsd/tests/auto-start-clean-runtime-db-gated.test.ts +3 -2
- package/src/resources/extensions/gsd/tests/auto-start-cold-db-bootstrap.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/auto-start-model-capture.test.ts +4 -3
- package/src/resources/extensions/gsd/tests/auto-start-worktree-db-path.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/auto-thinking-restore.test.ts +3 -2
- package/src/resources/extensions/gsd/tests/auto-warning-noise-regression.test.ts +3 -2
- package/src/resources/extensions/gsd/tests/bootstrap-derive-state-db-open.test.ts +2 -1
- package/src/resources/extensions/gsd/tests/canonical-milestone-root.test.ts +108 -0
- package/src/resources/extensions/gsd/tests/complete-slice-verification-gate.test.ts +2 -1
- package/src/resources/extensions/gsd/tests/context-store.test.ts +79 -0
- package/src/resources/extensions/gsd/tests/copy-planning-artifacts-samepath.test.ts +2 -1
- package/src/resources/extensions/gsd/tests/discuss-slice-structured-questions.test.ts +2 -1
- package/src/resources/extensions/gsd/tests/discuss-tool-scope-leak.test.ts +2 -1
- package/src/resources/extensions/gsd/tests/double-merge-guard.test.ts +4 -3
- package/src/resources/extensions/gsd/tests/empty-content-abort-loop.test.ts +4 -3
- package/src/resources/extensions/gsd/tests/finalize-timeout-guard.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/forensics-hook-key-parse.test.ts +2 -1
- package/src/resources/extensions/gsd/tests/forensics-worktree-telemetry.test.ts +145 -0
- package/src/resources/extensions/gsd/tests/idle-watchdog-stall-override.test.ts +6 -1
- package/src/resources/extensions/gsd/tests/import-done-milestones.test.ts +2 -1
- package/src/resources/extensions/gsd/tests/integration/all-milestones-complete-merge.test.ts +2 -1
- package/src/resources/extensions/gsd/tests/interactive-routing-bypass.test.ts +9 -3
- package/src/resources/extensions/gsd/tests/knowledge.test.ts +93 -1
- package/src/resources/extensions/gsd/tests/merge-conflict-stops-loop.test.ts +10 -3
- package/src/resources/extensions/gsd/tests/native-git-bridge-exec-fallback.test.ts +2 -1
- package/src/resources/extensions/gsd/tests/orphaned-worktree-audit.test.ts +59 -2
- package/src/resources/extensions/gsd/tests/parallel-research-dispatch.test.ts +4 -1
- package/src/resources/extensions/gsd/tests/preferences-worktree-sync.test.ts +2 -1
- package/src/resources/extensions/gsd/tests/prompt-cache-optimizer.test.ts +12 -0
- package/src/resources/extensions/gsd/tests/prompt-step-ordering.test.ts +15 -4
- package/src/resources/extensions/gsd/tests/queue-draft-detection.test.ts +3 -2
- package/src/resources/extensions/gsd/tests/queued-discuss-fast-path.test.ts +4 -5
- package/src/resources/extensions/gsd/tests/restore-tools-after-discuss.test.ts +6 -3
- package/src/resources/extensions/gsd/tests/sidecar-queue.test.ts +3 -2
- package/src/resources/extensions/gsd/tests/slice-cadence.test.ts +242 -0
- package/src/resources/extensions/gsd/tests/slice-context-injection.test.ts +3 -2
- package/src/resources/extensions/gsd/tests/smart-entry-draft.test.ts +2 -1
- package/src/resources/extensions/gsd/tests/stop-auto-race-null-unit.test.ts +3 -3
- package/src/resources/extensions/gsd/tests/subagent-model-dispatch.test.ts +7 -6
- package/src/resources/extensions/gsd/tests/sync-worktree-skip-current.test.ts +4 -3
- package/src/resources/extensions/gsd/tests/test-helpers.test.ts +147 -0
- package/src/resources/extensions/gsd/tests/test-helpers.ts +140 -0
- package/src/resources/extensions/gsd/tests/token-profile.test.ts +8 -1
- package/src/resources/extensions/gsd/tests/verify-artifact-tightened.test.ts +6 -5
- package/src/resources/extensions/gsd/tests/worktree-health-monorepo.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/worktree-nested-git-safety.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/worktree-submodule-safety.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/worktree-telemetry.test.ts +210 -0
- package/src/resources/extensions/gsd/tests/zombie-gsd-state.test.ts +3 -3
- package/src/resources/extensions/gsd/tools/validate-milestone.ts +8 -2
- package/src/resources/extensions/gsd/worktree-manager.ts +53 -0
- package/src/resources/extensions/gsd/worktree-resolver.ts +96 -9
- package/src/resources/extensions/gsd/worktree-telemetry.ts +322 -0
- /package/dist/web/standalone/.next/static/{vidAVJkURvTJ0_V2-64ro → Cev5xrAYA3ZGTRLyjR2fX}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{vidAVJkURvTJ0_V2-64ro → Cev5xrAYA3ZGTRLyjR2fX}/_ssgManifest.js +0 -0
|
@@ -15,7 +15,7 @@ import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync, realpathSy
|
|
|
15
15
|
import { join } from 'node:path';
|
|
16
16
|
import { tmpdir } from 'node:os';
|
|
17
17
|
import { GSD_ROOT_FILES, resolveGsdRootFile } from '../paths.ts';
|
|
18
|
-
import { inlineGsdRootFile } from '../auto-prompts.ts';
|
|
18
|
+
import { inlineGsdRootFile, inlineKnowledgeBudgeted } from '../auto-prompts.ts';
|
|
19
19
|
import { appendKnowledge } from '../files.ts';
|
|
20
20
|
import { loadKnowledgeBlock } from '../bootstrap/system-context.ts';
|
|
21
21
|
|
|
@@ -248,3 +248,95 @@ test('loadKnowledgeBlock: reports globalSizeKb above 4KB threshold', () => {
|
|
|
248
248
|
|
|
249
249
|
rmSync(tmp, { recursive: true, force: true });
|
|
250
250
|
});
|
|
251
|
+
|
|
252
|
+
// ─── inlineKnowledgeBudgeted — issue #4719 ─────────────────────────────────
|
|
253
|
+
// Milestone-phase prompts must not inject the full KNOWLEDGE.md. The budgeted
|
|
254
|
+
// helper scopes by milestone-level keywords and caps the injected size.
|
|
255
|
+
|
|
256
|
+
test('inlineKnowledgeBudgeted: returns scoped H3 entries for single-H2 file', async () => {
|
|
257
|
+
const tmp = realpathSync(mkdtempSync(join(tmpdir(), 'gsd-knowledge-')));
|
|
258
|
+
const gsdDir = join(tmp, '.gsd');
|
|
259
|
+
mkdirSync(gsdDir, { recursive: true });
|
|
260
|
+
|
|
261
|
+
const content = `# Project Knowledge
|
|
262
|
+
|
|
263
|
+
## Patterns
|
|
264
|
+
|
|
265
|
+
### Database: prepared statements
|
|
266
|
+
Always use prepared statements with SQLite.
|
|
267
|
+
|
|
268
|
+
### API: versioned paths
|
|
269
|
+
Use /v1/resource style versioning.
|
|
270
|
+
|
|
271
|
+
### Testing: node:test
|
|
272
|
+
Prefer node:test over external frameworks.
|
|
273
|
+
`;
|
|
274
|
+
writeFileSync(join(gsdDir, 'KNOWLEDGE.md'), content);
|
|
275
|
+
|
|
276
|
+
const result = await inlineKnowledgeBudgeted(tmp, ['database']);
|
|
277
|
+
assert.ok(result !== null, 'should return content');
|
|
278
|
+
assert.ok(result!.includes('Database: prepared statements'), 'includes matching H3');
|
|
279
|
+
assert.ok(!result!.includes('API: versioned paths'), 'excludes non-matching H3');
|
|
280
|
+
|
|
281
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
test('inlineKnowledgeBudgeted: caps payload below budget for large files', async () => {
|
|
285
|
+
const tmp = realpathSync(mkdtempSync(join(tmpdir(), 'gsd-knowledge-')));
|
|
286
|
+
const gsdDir = join(tmp, '.gsd');
|
|
287
|
+
mkdirSync(gsdDir, { recursive: true });
|
|
288
|
+
|
|
289
|
+
// Build a 200KB KNOWLEDGE with 500 H3 entries all matching 'shared'
|
|
290
|
+
const entries = Array.from({ length: 500 }, (_, i) =>
|
|
291
|
+
`### Entry ${i}: shared topic\n${'filler text '.repeat(30)}\n`,
|
|
292
|
+
).join('\n');
|
|
293
|
+
const content = `# Project Knowledge\n\n## Patterns\n\n${entries}`;
|
|
294
|
+
writeFileSync(join(gsdDir, 'KNOWLEDGE.md'), content);
|
|
295
|
+
|
|
296
|
+
const BUDGET_CHARS = 30_000;
|
|
297
|
+
const result = await inlineKnowledgeBudgeted(tmp, ['shared'], { maxChars: BUDGET_CHARS });
|
|
298
|
+
assert.ok(result !== null, 'should return content');
|
|
299
|
+
// Allow some overhead for header formatting, but must stay close to budget
|
|
300
|
+
assert.ok(
|
|
301
|
+
result!.length <= BUDGET_CHARS + 500,
|
|
302
|
+
`payload ${result!.length} chars should be <= budget ${BUDGET_CHARS} (+overhead)`,
|
|
303
|
+
);
|
|
304
|
+
// Far smaller than the raw file
|
|
305
|
+
assert.ok(
|
|
306
|
+
result!.length < content.length / 4,
|
|
307
|
+
`payload should be much smaller than full content (${content.length} chars)`,
|
|
308
|
+
);
|
|
309
|
+
assert.match(
|
|
310
|
+
result!,
|
|
311
|
+
/\[\.\.\.truncated \d+ chars; rerun with narrower scope if needed\]/,
|
|
312
|
+
'should include truncation note when budget is exceeded',
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
test('inlineKnowledgeBudgeted: returns null when no KNOWLEDGE.md exists', async () => {
|
|
319
|
+
const tmp = realpathSync(mkdtempSync(join(tmpdir(), 'gsd-knowledge-')));
|
|
320
|
+
const gsdDir = join(tmp, '.gsd');
|
|
321
|
+
mkdirSync(gsdDir, { recursive: true });
|
|
322
|
+
|
|
323
|
+
const result = await inlineKnowledgeBudgeted(tmp, ['database']);
|
|
324
|
+
assert.strictEqual(result, null);
|
|
325
|
+
|
|
326
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
test('inlineKnowledgeBudgeted: returns null when no entries match', async () => {
|
|
330
|
+
const tmp = realpathSync(mkdtempSync(join(tmpdir(), 'gsd-knowledge-')));
|
|
331
|
+
const gsdDir = join(tmp, '.gsd');
|
|
332
|
+
mkdirSync(gsdDir, { recursive: true });
|
|
333
|
+
writeFileSync(
|
|
334
|
+
join(gsdDir, 'KNOWLEDGE.md'),
|
|
335
|
+
'# Project Knowledge\n\n## Patterns\n\n### Database\nuse it\n',
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
const result = await inlineKnowledgeBudgeted(tmp, ['nonexistent']);
|
|
339
|
+
assert.strictEqual(result, null);
|
|
340
|
+
|
|
341
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
342
|
+
});
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import { readFileSync } from "node:fs";
|
|
12
12
|
import { join } from "node:path";
|
|
13
|
-
import {
|
|
13
|
+
import {createTestContext, extractSourceRegion } from "./test-helpers.ts";
|
|
14
14
|
|
|
15
15
|
const { assertTrue, report } = createTestContext();
|
|
16
16
|
|
|
@@ -27,7 +27,14 @@ console.log("\n=== #2330: Merge conflict stops auto loop ===");
|
|
|
27
27
|
const methodStart = resolverSrc.indexOf("Worktree-mode merge:");
|
|
28
28
|
assertTrue(methodStart > 0, "worktree-resolver has _mergeWorktreeMode method");
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
// Slice from the _mergeWorktreeMode docblock to the next method boundary
|
|
31
|
+
// (Branch-mode merge:) so that docblock/body growth doesn't silently drop
|
|
32
|
+
// the `throw err` out of the search window.
|
|
33
|
+
const methodEnd = resolverSrc.indexOf("Branch-mode merge:", methodStart);
|
|
34
|
+
const methodBody = resolverSrc.slice(
|
|
35
|
+
methodStart,
|
|
36
|
+
methodEnd > 0 ? methodEnd : methodStart + 8000,
|
|
37
|
+
);
|
|
31
38
|
const rethrowsConflict =
|
|
32
39
|
methodBody.includes("MergeConflictError") &&
|
|
33
40
|
methodBody.includes("throw err");
|
|
@@ -51,7 +58,7 @@ const instanceofIdx = phasesSrc.indexOf("instanceof MergeConflictError");
|
|
|
51
58
|
assertTrue(instanceofIdx > 0, "auto/phases.ts has instanceof MergeConflictError check");
|
|
52
59
|
|
|
53
60
|
if (instanceofIdx > 0) {
|
|
54
|
-
const afterHandler = phasesSrc
|
|
61
|
+
const afterHandler = extractSourceRegion(phasesSrc, "instanceof MergeConflictError");
|
|
55
62
|
const stopsLoop =
|
|
56
63
|
afterHandler.includes("stopAuto") ||
|
|
57
64
|
afterHandler.includes('action: "break"') ||
|
|
@@ -17,6 +17,7 @@ import { join } from "node:path";
|
|
|
17
17
|
import { tmpdir } from "node:os";
|
|
18
18
|
import { execFileSync } from "node:child_process";
|
|
19
19
|
import { nativeIsRepo, nativeCommit, nativeResetHard } from "../native-git-bridge.js";
|
|
20
|
+
import { extractSourceRegion } from "./test-helpers.ts";
|
|
20
21
|
|
|
21
22
|
// ─── Static analysis ──────────────────────────────────────────────────────
|
|
22
23
|
// Verify the fallback paths of the three affected functions do not call the
|
|
@@ -28,7 +29,7 @@ const SRC_PATH = join(import.meta.dirname, "..", "native-git-bridge.ts");
|
|
|
28
29
|
function extractFunctionBody(src: string, fnName: string): string {
|
|
29
30
|
const idx = src.indexOf(`export function ${fnName}`);
|
|
30
31
|
if (idx === -1) throw new Error(`${fnName} not found in source`);
|
|
31
|
-
return src
|
|
32
|
+
return extractSourceRegion(src, `export function ${fnName}`);
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
function hasRawExecSync(body: string): boolean {
|
|
@@ -107,7 +107,8 @@ describe("auditOrphanedMilestoneBranches", () => {
|
|
|
107
107
|
assert.ok(branches.includes("milestone/M001"), "unmerged branch must be preserved");
|
|
108
108
|
});
|
|
109
109
|
|
|
110
|
-
test("skips active (
|
|
110
|
+
test("skips active milestone branch with no commits ahead of main (nothing to recover)", () => {
|
|
111
|
+
// Branch created from main with zero divergence — no live work, nothing to warn about.
|
|
111
112
|
run("git branch milestone/M001", dir);
|
|
112
113
|
insertMilestone({ id: "M001", title: "Test", status: "active" });
|
|
113
114
|
|
|
@@ -116,11 +117,67 @@ describe("auditOrphanedMilestoneBranches", () => {
|
|
|
116
117
|
assert.deepStrictEqual(result.recovered, []);
|
|
117
118
|
assert.deepStrictEqual(result.warnings, []);
|
|
118
119
|
|
|
119
|
-
// Branch should still exist
|
|
120
|
+
// Branch should still exist (data safety — user may intend to use it)
|
|
120
121
|
const branches = run("git branch --list milestone/M001", dir);
|
|
121
122
|
assert.ok(branches.includes("milestone/M001"), "active milestone branch should be preserved");
|
|
122
123
|
});
|
|
123
124
|
|
|
125
|
+
test("#4762 — warns about in-progress milestone with unmerged commits ahead of main", () => {
|
|
126
|
+
// Simulates the primary #4761 scenario: auto-mode was interrupted mid-milestone.
|
|
127
|
+
// DB status = active/in_progress, branch has real work, main is behind.
|
|
128
|
+
run("git checkout -b milestone/M001", dir);
|
|
129
|
+
writeFileSync(join(dir, "feature.txt"), "in-progress work\n");
|
|
130
|
+
run("git add feature.txt", dir);
|
|
131
|
+
run("git commit -m \"in-progress work on M001\"", dir);
|
|
132
|
+
run("git checkout main", dir);
|
|
133
|
+
|
|
134
|
+
insertMilestone({ id: "M001", title: "Test", status: "active" });
|
|
135
|
+
|
|
136
|
+
const result = auditOrphanedMilestoneBranches(dir, "worktree");
|
|
137
|
+
|
|
138
|
+
// Must NOT recover/delete (data safety — work is live)
|
|
139
|
+
assert.deepStrictEqual(result.recovered, [], "must not delete a branch with live in-progress work");
|
|
140
|
+
|
|
141
|
+
// Must surface a warning so the user knows the worktree holds uncollapsed work
|
|
142
|
+
assert.ok(result.warnings.length > 0, "should warn about in-progress orphan");
|
|
143
|
+
assert.ok(
|
|
144
|
+
result.warnings.some(w => w.includes("milestone/M001") && w.includes("in-progress")),
|
|
145
|
+
`warning should mention milestone/M001 and in-progress state; got: ${JSON.stringify(result.warnings)}`,
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
// Branch must still exist
|
|
149
|
+
const branches = run("git branch --list milestone/M001", dir);
|
|
150
|
+
assert.ok(branches.includes("milestone/M001"), "in-progress branch must be preserved");
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("#4762 — also surfaces worktree directory for in-progress orphan when present", () => {
|
|
154
|
+
// In-progress + unmerged + physical worktree directory — the full primary scenario.
|
|
155
|
+
run("git checkout -b milestone/M001", dir);
|
|
156
|
+
writeFileSync(join(dir, "feature.txt"), "in-progress work\n");
|
|
157
|
+
run("git add feature.txt", dir);
|
|
158
|
+
run("git commit -m \"in-progress work on M001\"", dir);
|
|
159
|
+
run("git checkout main", dir);
|
|
160
|
+
|
|
161
|
+
// Simulate a leftover worktree directory
|
|
162
|
+
const wtDir = join(dir, ".gsd", "worktrees", "M001");
|
|
163
|
+
mkdirSync(wtDir, { recursive: true });
|
|
164
|
+
writeFileSync(join(wtDir, ".git"), `gitdir: ${join(dir, ".git", "worktrees", "M001")}\n`);
|
|
165
|
+
|
|
166
|
+
insertMilestone({ id: "M001", title: "Test", status: "active" });
|
|
167
|
+
|
|
168
|
+
const result = auditOrphanedMilestoneBranches(dir, "worktree");
|
|
169
|
+
|
|
170
|
+
// Must preserve everything for data safety
|
|
171
|
+
assert.deepStrictEqual(result.recovered, [], "must not touch worktree or branch with live work");
|
|
172
|
+
assert.ok(existsSync(wtDir), "worktree directory must be preserved");
|
|
173
|
+
|
|
174
|
+
// Warning should mention the worktree path so the user can find the work
|
|
175
|
+
assert.ok(
|
|
176
|
+
result.warnings.some(w => w.includes(".gsd/worktrees/M001") || w.includes("worktree")),
|
|
177
|
+
`warning should reference the worktree location; got: ${JSON.stringify(result.warnings)}`,
|
|
178
|
+
);
|
|
179
|
+
});
|
|
180
|
+
|
|
124
181
|
test("cleans up orphaned worktree directory for merged milestone", () => {
|
|
125
182
|
// Create milestone branch (merged — same as main)
|
|
126
183
|
run("git branch milestone/M001", dir);
|
|
@@ -12,6 +12,7 @@ import { tmpdir } from "node:os";
|
|
|
12
12
|
import { fileURLToPath } from "node:url";
|
|
13
13
|
|
|
14
14
|
import { resolveDispatch } from "../auto-dispatch.ts";
|
|
15
|
+
import { extractSourceRegion } from "./test-helpers.ts";
|
|
15
16
|
|
|
16
17
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
17
18
|
|
|
@@ -79,7 +80,9 @@ test("dispatch: parallel-research-slices requires 2+ slices", () => {
|
|
|
79
80
|
|
|
80
81
|
test("dispatch: parallel-research-slices respects skip_research", () => {
|
|
81
82
|
const ruleIdx = dispatchSrc.indexOf("parallel-research-slices");
|
|
82
|
-
|
|
83
|
+
// Pin to the located occurrence — if "parallel-research-slices" appears
|
|
84
|
+
// more than once in the source, fromIdx keeps the anchor deterministic.
|
|
85
|
+
const ruleBlock = extractSourceRegion(dispatchSrc, "parallel-research-slices", { fromIdx: ruleIdx });
|
|
83
86
|
assert.ok(
|
|
84
87
|
ruleBlock.includes("skip_research") || dispatchSrc.slice(ruleIdx - 300, ruleIdx).includes("skip_research"),
|
|
85
88
|
"rule should check skip_research preference",
|
|
@@ -12,6 +12,7 @@ import assert from "node:assert/strict";
|
|
|
12
12
|
import { readFileSync, mkdtempSync, mkdirSync, writeFileSync, existsSync, readdirSync, rmSync } from "node:fs";
|
|
13
13
|
import { join } from "node:path";
|
|
14
14
|
import { tmpdir } from "node:os";
|
|
15
|
+
import { extractSourceRegion } from "./test-helpers.ts";
|
|
15
16
|
|
|
16
17
|
test("#2684: preferences files are NOT in ROOT_STATE_FILES (forward-only sync)", () => {
|
|
17
18
|
const srcPath = join(import.meta.dirname, "..", "auto-worktree.ts");
|
|
@@ -49,7 +50,7 @@ test("copyPlanningArtifacts prefers canonical PREFERENCES.md with lowercase fall
|
|
|
49
50
|
assert.ok(fnIdx !== -1, "copyPlanningArtifacts function exists");
|
|
50
51
|
|
|
51
52
|
// Extract function body (up to the next top-level function)
|
|
52
|
-
const fnBody = src
|
|
53
|
+
const fnBody = extractSourceRegion(src, "function copyPlanningArtifacts");
|
|
53
54
|
|
|
54
55
|
assert.ok(
|
|
55
56
|
fnBody.includes("PROJECT_PREFERENCES_FILE") && fnBody.includes("LEGACY_PROJECT_PREFERENCES_FILE"),
|
|
@@ -64,6 +64,18 @@ describe("prompt-cache-optimizer: classifySection", () => {
|
|
|
64
64
|
assert.equal(classifySection("overrides"), "semi-static");
|
|
65
65
|
});
|
|
66
66
|
|
|
67
|
+
// Regression: issue #4719 — KNOWLEDGE falls through to dynamic default.
|
|
68
|
+
// Knowledge content is reused across all tasks within a milestone, so it
|
|
69
|
+
// must be classified as semi-static to qualify for prefix caching when the
|
|
70
|
+
// cache optimizer is wired into the prompt path.
|
|
71
|
+
it("classifies knowledge as semi-static (issue #4719)", () => {
|
|
72
|
+
assert.equal(classifySection("knowledge"), "semi-static");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("classifies project-knowledge as semi-static (issue #4719)", () => {
|
|
76
|
+
assert.equal(classifySection("project-knowledge"), "semi-static");
|
|
77
|
+
});
|
|
78
|
+
|
|
67
79
|
it("classifies task-plan as dynamic", () => {
|
|
68
80
|
assert.equal(classifySection("task-plan"), "dynamic");
|
|
69
81
|
});
|
|
@@ -13,6 +13,7 @@ import assert from 'node:assert/strict';
|
|
|
13
13
|
import { readFileSync } from 'node:fs';
|
|
14
14
|
import { fileURLToPath } from 'node:url';
|
|
15
15
|
import { dirname, join } from 'node:path';
|
|
16
|
+
import { extractSourceRegion } from "./test-helpers.ts";
|
|
16
17
|
|
|
17
18
|
const __filename = fileURLToPath(import.meta.url);
|
|
18
19
|
const __dirname = dirname(__filename);
|
|
@@ -69,11 +70,21 @@ describe('register-extension _gsdEpipeGuard (#3696)', () => {
|
|
|
69
70
|
|
|
70
71
|
describe('register-hooks session_before_compact (#3696)', () => {
|
|
71
72
|
test('session_before_compact only checks isAutoActive', () => {
|
|
72
|
-
//
|
|
73
|
-
|
|
73
|
+
// Anchor on the full registration token rather than the bare event name —
|
|
74
|
+
// prevents matching unrelated substring occurrences.
|
|
75
|
+
const compactIdx = registerHooksSrc.indexOf('pi.on("session_before_compact"');
|
|
74
76
|
assert.ok(compactIdx > -1, 'session_before_compact hook should exist');
|
|
75
|
-
// The first check in the handler should be isAutoActive(), not isAutoPaused()
|
|
76
|
-
|
|
77
|
+
// The first check in the handler should be isAutoActive(), not isAutoPaused().
|
|
78
|
+
// Bound the region to this single handler — register-hooks.ts contains
|
|
79
|
+
// multiple pi.on("session_before_compact") handlers and a later handler
|
|
80
|
+
// legitimately references isAutoPaused.
|
|
81
|
+
const afterCompact = extractSourceRegion(
|
|
82
|
+
registerHooksSrc,
|
|
83
|
+
'pi.on("session_before_compact"',
|
|
84
|
+
'pi.on("',
|
|
85
|
+
// NB: endAnchor search starts AFTER the startAnchor, so the next
|
|
86
|
+
// pi.on("... matches the subsequent handler rather than this one.
|
|
87
|
+
);
|
|
77
88
|
assert.match(afterCompact, /isAutoActive\(\)/,
|
|
78
89
|
'session_before_compact should check isAutoActive()');
|
|
79
90
|
// Should NOT block compaction when paused
|
|
@@ -6,6 +6,7 @@ import { tmpdir } from "node:os";
|
|
|
6
6
|
|
|
7
7
|
import { deriveState } from "../state.js";
|
|
8
8
|
import { buildExistingMilestonesContext } from "../guided-flow.js";
|
|
9
|
+
import { extractSourceRegion } from "./test-helpers.ts";
|
|
9
10
|
|
|
10
11
|
describe('queue-draft-detection', () => {
|
|
11
12
|
test('draft and context milestone detection', async () => {
|
|
@@ -68,7 +69,7 @@ describe('queue-draft-detection', () => {
|
|
|
68
69
|
|
|
69
70
|
// both files: CONTEXT.md wins, no draft label
|
|
70
71
|
const m003Idx = context.indexOf("M003:");
|
|
71
|
-
const m003Section = context
|
|
72
|
+
const m003Section = extractSourceRegion(context, "M003:");
|
|
72
73
|
assert.ok(
|
|
73
74
|
m003Section.includes("**Context:**"),
|
|
74
75
|
"M003 (both files) should use 'Context:' label (CONTEXT.md wins)",
|
|
@@ -84,7 +85,7 @@ describe('queue-draft-detection', () => {
|
|
|
84
85
|
|
|
85
86
|
// neither file: no context section
|
|
86
87
|
const m004Idx = context.indexOf("M004:");
|
|
87
|
-
const m004Section = context
|
|
88
|
+
const m004Section = extractSourceRegion(context, "M004:");
|
|
88
89
|
assert.ok(
|
|
89
90
|
!m004Section.includes("**Context:**"),
|
|
90
91
|
"M004 (neither file) should not have Context: label",
|
|
@@ -3,6 +3,7 @@ import assert from "node:assert/strict";
|
|
|
3
3
|
import { readFileSync } from "node:fs";
|
|
4
4
|
import { join, dirname } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { extractSourceRegion } from "./test-helpers.ts";
|
|
6
7
|
|
|
7
8
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
9
|
|
|
@@ -28,7 +29,7 @@ describe("queued-discuss-fast-path", () => {
|
|
|
28
29
|
const fnStart = source.indexOf("async function dispatchDiscussForMilestone(");
|
|
29
30
|
assert.ok(fnStart > 0, "dispatchDiscussForMilestone must exist");
|
|
30
31
|
const fnEnd = source.indexOf("\nasync function ", fnStart + 1);
|
|
31
|
-
const fnBody =
|
|
32
|
+
const fnBody = extractSourceRegion(source, "async function dispatchDiscussForMilestone(");
|
|
32
33
|
assert.ok(
|
|
33
34
|
fnBody.includes("fastPathInstruction"),
|
|
34
35
|
"dispatchDiscussForMilestone must compute fastPathInstruction",
|
|
@@ -61,8 +62,7 @@ describe("queued-discuss-fast-path", () => {
|
|
|
61
62
|
const source = guidedFlowSrc();
|
|
62
63
|
const fnStart = source.indexOf("async function showDiscussQueuedMilestone(");
|
|
63
64
|
assert.ok(fnStart > 0, "showDiscussQueuedMilestone must exist");
|
|
64
|
-
const
|
|
65
|
-
const fnBody = fnEnd > 0 ? source.slice(fnStart, fnEnd) : source.slice(fnStart, fnStart + 3000);
|
|
65
|
+
const fnBody = extractSourceRegion(source, "async function showDiscussQueuedMilestone(", "\nasync function ");
|
|
66
66
|
assert.ok(
|
|
67
67
|
fnBody.includes("hasDraft"),
|
|
68
68
|
"showDiscussQueuedMilestone must check hasDraft",
|
|
@@ -81,8 +81,7 @@ describe("queued-discuss-fast-path", () => {
|
|
|
81
81
|
const source = guidedFlowSrc();
|
|
82
82
|
const fnStart = source.indexOf("async function showDiscussQueuedMilestone(");
|
|
83
83
|
assert.ok(fnStart > 0, "showDiscussQueuedMilestone must exist");
|
|
84
|
-
const
|
|
85
|
-
const fnBody = fnEnd > 0 ? source.slice(fnStart, fnEnd) : source.slice(fnStart, fnStart + 3000);
|
|
84
|
+
const fnBody = extractSourceRegion(source, "async function showDiscussQueuedMilestone(", "\nasync function ");
|
|
86
85
|
assert.ok(
|
|
87
86
|
fnBody.includes("let fastPath = hasDraft"),
|
|
88
87
|
"showDiscussQueuedMilestone must set fastPath = hasDraft so draft presence auto-enables fast path",
|
|
@@ -13,6 +13,7 @@ import { describe, it } from 'node:test'
|
|
|
13
13
|
import assert from 'node:assert/strict'
|
|
14
14
|
import { readFileSync } from 'node:fs'
|
|
15
15
|
import { resolve } from 'node:path'
|
|
16
|
+
import { extractSourceRegion } from "./test-helpers.ts";
|
|
16
17
|
|
|
17
18
|
const src = readFileSync(
|
|
18
19
|
resolve(process.cwd(), 'src', 'resources', 'extensions', 'gsd', 'guided-flow.ts'),
|
|
@@ -37,7 +38,7 @@ describe('restore tools after discuss flow scoping (#3628)', () => {
|
|
|
37
38
|
assert.ok(discussCheck !== -1)
|
|
38
39
|
|
|
39
40
|
// Look for savedTools assignment within the discuss block
|
|
40
|
-
const blockAfter = src
|
|
41
|
+
const blockAfter = extractSourceRegion(src, 'if (unitType?.startsWith("discuss-"))')
|
|
41
42
|
assert.ok(
|
|
42
43
|
blockAfter.includes('savedTools = currentTools'),
|
|
43
44
|
'savedTools must be assigned from currentTools inside the discuss block',
|
|
@@ -55,8 +56,10 @@ describe('restore tools after discuss flow scoping (#3628)', () => {
|
|
|
55
56
|
const sendMsg = src.indexOf('triggerTurn: true', savedToolsAssign)
|
|
56
57
|
assert.ok(sendMsg !== -1, 'discuss-flow sendMessage with triggerTurn must exist after savedTools capture')
|
|
57
58
|
|
|
58
|
-
// After sendMessage, savedTools should be restored via setActiveTools
|
|
59
|
-
|
|
59
|
+
// After sendMessage, savedTools should be restored via setActiveTools.
|
|
60
|
+
// Use fromIdx to anchor at the discuss-flow sendMessage, not the first
|
|
61
|
+
// triggerTurn: true occurrence in the file.
|
|
62
|
+
const afterSend = extractSourceRegion(src, 'triggerTurn: true', { fromIdx: savedToolsAssign })
|
|
60
63
|
assert.ok(
|
|
61
64
|
afterSend.includes('if (savedTools)'),
|
|
62
65
|
'savedTools restoration guard must exist after sendMessage',
|
|
@@ -11,6 +11,7 @@ import assert from "node:assert/strict";
|
|
|
11
11
|
import { readFileSync } from "node:fs";
|
|
12
12
|
import { join, dirname } from "node:path";
|
|
13
13
|
import { fileURLToPath } from "node:url";
|
|
14
|
+
import { extractSourceRegion } from "./test-helpers.ts";
|
|
14
15
|
|
|
15
16
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
16
17
|
const SESSION_TS_PATH = join(__dirname, "..", "auto", "session.ts");
|
|
@@ -52,7 +53,7 @@ test("SidecarItem type is exported from session.ts", () => {
|
|
|
52
53
|
test("SidecarItem has required kind field with hook/triage/quick-task union", () => {
|
|
53
54
|
const source = getSessionTsSource();
|
|
54
55
|
const ifaceIdx = source.indexOf("export interface SidecarItem");
|
|
55
|
-
const ifaceBlock = source
|
|
56
|
+
const ifaceBlock = extractSourceRegion(source, "export interface SidecarItem");
|
|
56
57
|
assert.ok(
|
|
57
58
|
ifaceBlock.includes('"hook"') && ifaceBlock.includes('"triage"') && ifaceBlock.includes('"quick-task"'),
|
|
58
59
|
"SidecarItem.kind must be a union of 'hook' | 'triage' | 'quick-task'",
|
|
@@ -77,7 +78,7 @@ test("AutoSession resets sidecarQueue in reset()", () => {
|
|
|
77
78
|
const source = getSessionTsSource();
|
|
78
79
|
const resetIdx = source.indexOf("reset(): void");
|
|
79
80
|
assert.ok(resetIdx > -1, "AutoSession must have a reset() method");
|
|
80
|
-
const resetBlock = source
|
|
81
|
+
const resetBlock = extractSourceRegion(source, "reset(): void");
|
|
81
82
|
assert.ok(
|
|
82
83
|
resetBlock.includes("sidecarQueue"),
|
|
83
84
|
"reset() must clear sidecarQueue",
|