gsd-pi 2.47.0-dev.04be8c9 → 2.47.0-dev.f2e721d

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.
Files changed (65) hide show
  1. package/dist/resources/extensions/gsd/auto-start.js +8 -1
  2. package/dist/resources/extensions/gsd/forensics.js +292 -1
  3. package/dist/resources/extensions/gsd/guided-flow.js +85 -3
  4. package/dist/resources/extensions/gsd/prompts/forensics.md +37 -5
  5. package/dist/resources/extensions/gsd/session-forensics.js +10 -1
  6. package/dist/web/standalone/.next/BUILD_ID +1 -1
  7. package/dist/web/standalone/.next/app-path-routes-manifest.json +17 -17
  8. package/dist/web/standalone/.next/build-manifest.json +2 -2
  9. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  10. package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
  11. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  12. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  13. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  14. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  15. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  16. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  17. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  18. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  19. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  20. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  21. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  22. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  23. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  24. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  25. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  26. package/dist/web/standalone/.next/server/app/api/forensics/route.js +1 -1
  27. package/dist/web/standalone/.next/server/app/index.html +1 -1
  28. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  29. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  30. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  31. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  32. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app-paths-manifest.json +17 -17
  35. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  36. package/dist/web/standalone/.next/server/pages/500.html +2 -2
  37. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  38. package/package.json +1 -1
  39. package/packages/pi-agent-core/dist/agent-loop.js +3 -2
  40. package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
  41. package/packages/pi-agent-core/src/agent-loop.ts +3 -2
  42. package/packages/pi-coding-agent/dist/core/model-registry-auth-mode.test.js +43 -0
  43. package/packages/pi-coding-agent/dist/core/model-registry-auth-mode.test.js.map +1 -1
  44. package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
  45. package/packages/pi-coding-agent/dist/core/model-registry.js +26 -3
  46. package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
  47. package/packages/pi-coding-agent/src/core/model-registry-auth-mode.test.ts +70 -0
  48. package/packages/pi-coding-agent/src/core/model-registry.ts +29 -2
  49. package/packages/pi-tui/dist/components/box.d.ts +1 -0
  50. package/packages/pi-tui/dist/components/box.d.ts.map +1 -1
  51. package/packages/pi-tui/dist/components/box.js +10 -0
  52. package/packages/pi-tui/dist/components/box.js.map +1 -1
  53. package/packages/pi-tui/src/components/box.ts +10 -0
  54. package/src/resources/extensions/gsd/auto-start.ts +7 -1
  55. package/src/resources/extensions/gsd/forensics.ts +329 -2
  56. package/src/resources/extensions/gsd/guided-flow.ts +105 -3
  57. package/src/resources/extensions/gsd/prompts/forensics.md +37 -5
  58. package/src/resources/extensions/gsd/session-forensics.ts +11 -1
  59. package/src/resources/extensions/gsd/tests/discuss-queued-milestones.test.ts +241 -0
  60. package/src/resources/extensions/gsd/tests/forensics-error-filter.test.ts +121 -0
  61. package/src/resources/extensions/gsd/tests/forensics-journal.test.ts +162 -0
  62. package/src/resources/extensions/gsd/tests/preflight-context-draft-filter.test.ts +115 -0
  63. package/src/resources/extensions/gsd/tests/stale-milestone-id-reservation.test.ts +79 -0
  64. /package/dist/web/standalone/.next/static/{GR9tXQAPXXBL4AUugDPlJ → O3E7X3EJ2lEKs_0hIUzGd}/_buildManifest.js +0 -0
  65. /package/dist/web/standalone/.next/static/{GR9tXQAPXXBL4AUugDPlJ → O3E7X3EJ2lEKs_0hIUzGd}/_ssgManifest.js +0 -0
@@ -572,3 +572,73 @@ describe("ModelRegistry authMode — streamSimple apiKey boundary", () => {
572
572
  assert.equal((captured as Record<string, unknown>).reasoning, "high", "reasoning must pass through");
573
573
  });
574
574
  });
575
+
576
+ // ─── Provider-scoped stream routing (#2533) ───────────────────────────────────
577
+
578
+ describe("ModelRegistry authMode — provider-scoped stream routing", () => {
579
+ it("does not clobber built-in stream handler when custom provider uses same api", () => {
580
+ const registry = createRegistry(() => true);
581
+ const customSpy = createStreamSpy();
582
+
583
+ // Register a custom provider with the same API type as a built-in (anthropic-messages).
584
+ // This simulates the claude-code-cli extension registering with api: "anthropic-messages".
585
+ registry.registerProvider("custom-cli", {
586
+ authMode: "externalCli",
587
+ baseUrl: "local://custom",
588
+ api: "anthropic-messages",
589
+ streamSimple: customSpy.streamSimple,
590
+ models: [createProviderModel("custom-model", "anthropic-messages")],
591
+ });
592
+
593
+ // The built-in anthropic-messages provider should still be accessible
594
+ // when calling streamSimple with a model from the built-in provider.
595
+ const provider = getApiProvider("anthropic-messages" as Api);
596
+ assert.ok(provider, "anthropic-messages provider must still be registered");
597
+
598
+ // Call with a built-in anthropic model — should NOT hit the custom spy.
599
+ // The built-in handler will throw (no API key), which proves the routing
600
+ // correctly delegates to the built-in instead of the custom handler.
601
+ assert.throws(
602
+ () => provider.streamSimple(
603
+ makeModel("anthropic", "claude-sonnet-4-6", "anthropic-messages"),
604
+ makeContext(),
605
+ { maxTokens: 4096 } as SimpleStreamOptions,
606
+ ),
607
+ (err: Error) => err.message.includes("API key"),
608
+ "built-in Anthropic handler must be invoked (throws because no API key in tests)",
609
+ );
610
+
611
+ assert.equal(
612
+ customSpy.getCapturedOptions(),
613
+ undefined,
614
+ "custom provider's streamSimple must NOT be called for anthropic provider models",
615
+ );
616
+ });
617
+
618
+ it("routes to custom provider when model.provider matches", () => {
619
+ const registry = createRegistry(() => true);
620
+ const customSpy = createStreamSpy();
621
+
622
+ registry.registerProvider("custom-cli", {
623
+ authMode: "externalCli",
624
+ baseUrl: "local://custom",
625
+ api: "anthropic-messages",
626
+ streamSimple: customSpy.streamSimple,
627
+ models: [createProviderModel("custom-model", "anthropic-messages")],
628
+ });
629
+
630
+ const provider = getApiProvider("anthropic-messages" as Api);
631
+ assert.ok(provider);
632
+
633
+ // Call with the custom provider's model — should hit the custom spy
634
+ provider.streamSimple(
635
+ makeModel("custom-cli", "custom-model", "anthropic-messages"),
636
+ makeContext(),
637
+ { maxTokens: 2048 } as SimpleStreamOptions,
638
+ );
639
+
640
+ const captured = customSpy.getCapturedOptions();
641
+ assert.ok(captured, "custom provider's streamSimple must be called for its own models");
642
+ assert.equal(captured.maxTokens, 2048);
643
+ });
644
+ });
@@ -6,6 +6,7 @@ import {
6
6
  type Api,
7
7
  type AssistantMessageEventStream,
8
8
  type Context,
9
+ getApiProvider,
9
10
  getModels,
10
11
  getProviders,
11
12
  type KnownProvider,
@@ -635,11 +636,37 @@ export class ModelRegistry {
635
636
  })
636
637
  : rawStreamSimple;
637
638
 
639
+ // Guard: if there's already a handler registered for this API, wrap
640
+ // the new one so it only fires for models from this provider and
641
+ // delegates to the previous handler for all other providers. Without
642
+ // this, a custom provider using api:"anthropic-messages" would clobber
643
+ // the built-in Anthropic stream handler (#2536).
644
+ const existingProvider = getApiProvider(config.api as Api);
645
+ const scopedStream = existingProvider
646
+ ? (model: Model<Api>, context: Context, options?: SimpleStreamOptions): AssistantMessageEventStream => {
647
+ if (model.provider === providerName) {
648
+ return streamSimple(model, context, options);
649
+ }
650
+ return existingProvider.streamSimple(model, context, options);
651
+ }
652
+ : streamSimple;
653
+
654
+ const newFullStream = (model: Model<Api>, context: Context, options?: SimpleStreamOptions) =>
655
+ scopedStream(model, context, options as SimpleStreamOptions);
656
+ const scopedFullStream = existingProvider
657
+ ? (model: Model<Api>, context: Context, options?: Record<string, unknown>) => {
658
+ if (model.provider === providerName) {
659
+ return newFullStream(model, context, options as SimpleStreamOptions);
660
+ }
661
+ return existingProvider.stream(model, context, options);
662
+ }
663
+ : newFullStream;
664
+
638
665
  registerApiProvider(
639
666
  {
640
667
  api: config.api,
641
- stream: (model, context, options) => streamSimple(model, context, options as SimpleStreamOptions),
642
- streamSimple,
668
+ stream: scopedFullStream as any,
669
+ streamSimple: scopedStream,
643
670
  },
644
671
  `provider:${providerName}`,
645
672
  );
@@ -10,6 +10,7 @@ export declare class Box implements Component {
10
10
  private cache?;
11
11
  constructor(paddingX?: number, paddingY?: number, bgFn?: (text: string) => string);
12
12
  addChild(component: Component): void;
13
+ insertChildBefore(component: Component, before: Component): void;
13
14
  removeChild(component: Component): void;
14
15
  clear(): void;
15
16
  setBgFn(bgFn?: (text: string) => string): void;
@@ -1 +1 @@
1
- {"version":3,"file":"box.d.ts","sourceRoot":"","sources":["../../src/components/box.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAU3C;;GAEG;AACH,qBAAa,GAAI,YAAW,SAAS;IACpC,QAAQ,EAAE,SAAS,EAAE,CAAM;IAC3B,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,IAAI,CAAC,CAA2B;IAGxC,OAAO,CAAC,KAAK,CAAC,CAAc;gBAEhB,QAAQ,SAAI,EAAE,QAAQ,SAAI,EAAE,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM;IAMvE,QAAQ,CAAC,SAAS,EAAE,SAAS,GAAG,IAAI;IAKpC,WAAW,CAAC,SAAS,EAAE,SAAS,GAAG,IAAI;IAQvC,KAAK,IAAI,IAAI;IAKb,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI;IAK9C,OAAO,CAAC,eAAe;IAIvB,OAAO,CAAC,UAAU;IAWlB,UAAU,IAAI,IAAI;IAOlB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE;IAqD/B,OAAO,CAAC,OAAO;CAUf"}
1
+ {"version":3,"file":"box.d.ts","sourceRoot":"","sources":["../../src/components/box.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAU3C;;GAEG;AACH,qBAAa,GAAI,YAAW,SAAS;IACpC,QAAQ,EAAE,SAAS,EAAE,CAAM;IAC3B,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,IAAI,CAAC,CAA2B;IAGxC,OAAO,CAAC,KAAK,CAAC,CAAc;gBAEhB,QAAQ,SAAI,EAAE,QAAQ,SAAI,EAAE,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM;IAMvE,QAAQ,CAAC,SAAS,EAAE,SAAS,GAAG,IAAI;IAKpC,iBAAiB,CAAC,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,GAAG,IAAI;IAUhE,WAAW,CAAC,SAAS,EAAE,SAAS,GAAG,IAAI;IAQvC,KAAK,IAAI,IAAI;IAKb,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI;IAK9C,OAAO,CAAC,eAAe;IAIvB,OAAO,CAAC,UAAU;IAWlB,UAAU,IAAI,IAAI;IAOlB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE;IAqD/B,OAAO,CAAC,OAAO;CAUf"}
@@ -13,6 +13,16 @@ export class Box {
13
13
  this.children.push(component);
14
14
  this.invalidateCache();
15
15
  }
16
+ insertChildBefore(component, before) {
17
+ const index = this.children.indexOf(before);
18
+ if (index !== -1) {
19
+ this.children.splice(index, 0, component);
20
+ }
21
+ else {
22
+ this.children.push(component);
23
+ }
24
+ this.invalidateCache();
25
+ }
16
26
  removeChild(component) {
17
27
  const index = this.children.indexOf(component);
18
28
  if (index !== -1) {
@@ -1 +1 @@
1
- {"version":3,"file":"box.js","sourceRoot":"","sources":["../../src/components/box.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Component } from \"../tui.js\";\nimport { applyBackgroundToLine, visibleWidth } from \"../utils.js\";\n\ntype RenderCache = {\n\tchildLines: string[];\n\twidth: number;\n\tbgSample: string | undefined;\n\tlines: string[];\n};\n\n/**\n * Box component - a container that applies padding and background to all children\n */\nexport class Box implements Component {\n\tchildren: Component[] = [];\n\tprivate paddingX: number;\n\tprivate paddingY: number;\n\tprivate bgFn?: (text: string) => string;\n\n\t// Cache for rendered output\n\tprivate cache?: RenderCache;\n\n\tconstructor(paddingX = 1, paddingY = 1, bgFn?: (text: string) => string) {\n\t\tthis.paddingX = paddingX;\n\t\tthis.paddingY = paddingY;\n\t\tthis.bgFn = bgFn;\n\t}\n\n\taddChild(component: Component): void {\n\t\tthis.children.push(component);\n\t\tthis.invalidateCache();\n\t}\n\n\tremoveChild(component: Component): void {\n\t\tconst index = this.children.indexOf(component);\n\t\tif (index !== -1) {\n\t\t\tthis.children.splice(index, 1);\n\t\t\tthis.invalidateCache();\n\t\t}\n\t}\n\n\tclear(): void {\n\t\tthis.children = [];\n\t\tthis.invalidateCache();\n\t}\n\n\tsetBgFn(bgFn?: (text: string) => string): void {\n\t\tthis.bgFn = bgFn;\n\t\t// Don't invalidate here - we'll detect bgFn changes by sampling output\n\t}\n\n\tprivate invalidateCache(): void {\n\t\tthis.cache = undefined;\n\t}\n\n\tprivate matchCache(width: number, childLines: string[], bgSample: string | undefined): boolean {\n\t\tconst cache = this.cache;\n\t\treturn (\n\t\t\t!!cache &&\n\t\t\tcache.width === width &&\n\t\t\tcache.bgSample === bgSample &&\n\t\t\tcache.childLines.length === childLines.length &&\n\t\t\tcache.childLines.every((line, i) => line === childLines[i])\n\t\t);\n\t}\n\n\tinvalidate(): void {\n\t\tthis.invalidateCache();\n\t\tfor (const child of this.children) {\n\t\t\tchild.invalidate?.();\n\t\t}\n\t}\n\n\trender(width: number): string[] {\n\t\tif (this.children.length === 0) {\n\t\t\treturn [];\n\t\t}\n\n\t\tconst contentWidth = Math.max(1, width - this.paddingX * 2);\n\t\tconst leftPad = \" \".repeat(this.paddingX);\n\n\t\t// Render all children\n\t\tconst childLines: string[] = [];\n\t\tfor (const child of this.children) {\n\t\t\tconst lines = child.render(contentWidth);\n\t\t\tfor (const line of lines) {\n\t\t\t\tchildLines.push(leftPad + line);\n\t\t\t}\n\t\t}\n\n\t\tif (childLines.length === 0) {\n\t\t\treturn [];\n\t\t}\n\n\t\t// Check if bgFn output changed by sampling\n\t\tconst bgSample = this.bgFn ? this.bgFn(\"test\") : undefined;\n\n\t\t// Check cache validity\n\t\tif (this.matchCache(width, childLines, bgSample)) {\n\t\t\treturn this.cache!.lines;\n\t\t}\n\n\t\t// Apply background and padding\n\t\tconst result: string[] = [];\n\n\t\t// Top padding\n\t\tfor (let i = 0; i < this.paddingY; i++) {\n\t\t\tresult.push(this.applyBg(\"\", width));\n\t\t}\n\n\t\t// Content\n\t\tfor (const line of childLines) {\n\t\t\tresult.push(this.applyBg(line, width));\n\t\t}\n\n\t\t// Bottom padding\n\t\tfor (let i = 0; i < this.paddingY; i++) {\n\t\t\tresult.push(this.applyBg(\"\", width));\n\t\t}\n\n\t\t// Update cache\n\t\tthis.cache = { childLines, width, bgSample, lines: result };\n\n\t\treturn result;\n\t}\n\n\tprivate applyBg(line: string, width: number): string {\n\t\tconst visLen = visibleWidth(line);\n\t\tconst padNeeded = Math.max(0, width - visLen);\n\t\tconst padded = line + \" \".repeat(padNeeded);\n\n\t\tif (this.bgFn) {\n\t\t\treturn applyBackgroundToLine(padded, width, this.bgFn);\n\t\t}\n\t\treturn padded;\n\t}\n}\n"]}
1
+ {"version":3,"file":"box.js","sourceRoot":"","sources":["../../src/components/box.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Component } from \"../tui.js\";\nimport { applyBackgroundToLine, visibleWidth } from \"../utils.js\";\n\ntype RenderCache = {\n\tchildLines: string[];\n\twidth: number;\n\tbgSample: string | undefined;\n\tlines: string[];\n};\n\n/**\n * Box component - a container that applies padding and background to all children\n */\nexport class Box implements Component {\n\tchildren: Component[] = [];\n\tprivate paddingX: number;\n\tprivate paddingY: number;\n\tprivate bgFn?: (text: string) => string;\n\n\t// Cache for rendered output\n\tprivate cache?: RenderCache;\n\n\tconstructor(paddingX = 1, paddingY = 1, bgFn?: (text: string) => string) {\n\t\tthis.paddingX = paddingX;\n\t\tthis.paddingY = paddingY;\n\t\tthis.bgFn = bgFn;\n\t}\n\n\taddChild(component: Component): void {\n\t\tthis.children.push(component);\n\t\tthis.invalidateCache();\n\t}\n\n\tinsertChildBefore(component: Component, before: Component): void {\n\t\tconst index = this.children.indexOf(before);\n\t\tif (index !== -1) {\n\t\t\tthis.children.splice(index, 0, component);\n\t\t} else {\n\t\t\tthis.children.push(component);\n\t\t}\n\t\tthis.invalidateCache();\n\t}\n\n\tremoveChild(component: Component): void {\n\t\tconst index = this.children.indexOf(component);\n\t\tif (index !== -1) {\n\t\t\tthis.children.splice(index, 1);\n\t\t\tthis.invalidateCache();\n\t\t}\n\t}\n\n\tclear(): void {\n\t\tthis.children = [];\n\t\tthis.invalidateCache();\n\t}\n\n\tsetBgFn(bgFn?: (text: string) => string): void {\n\t\tthis.bgFn = bgFn;\n\t\t// Don't invalidate here - we'll detect bgFn changes by sampling output\n\t}\n\n\tprivate invalidateCache(): void {\n\t\tthis.cache = undefined;\n\t}\n\n\tprivate matchCache(width: number, childLines: string[], bgSample: string | undefined): boolean {\n\t\tconst cache = this.cache;\n\t\treturn (\n\t\t\t!!cache &&\n\t\t\tcache.width === width &&\n\t\t\tcache.bgSample === bgSample &&\n\t\t\tcache.childLines.length === childLines.length &&\n\t\t\tcache.childLines.every((line, i) => line === childLines[i])\n\t\t);\n\t}\n\n\tinvalidate(): void {\n\t\tthis.invalidateCache();\n\t\tfor (const child of this.children) {\n\t\t\tchild.invalidate?.();\n\t\t}\n\t}\n\n\trender(width: number): string[] {\n\t\tif (this.children.length === 0) {\n\t\t\treturn [];\n\t\t}\n\n\t\tconst contentWidth = Math.max(1, width - this.paddingX * 2);\n\t\tconst leftPad = \" \".repeat(this.paddingX);\n\n\t\t// Render all children\n\t\tconst childLines: string[] = [];\n\t\tfor (const child of this.children) {\n\t\t\tconst lines = child.render(contentWidth);\n\t\t\tfor (const line of lines) {\n\t\t\t\tchildLines.push(leftPad + line);\n\t\t\t}\n\t\t}\n\n\t\tif (childLines.length === 0) {\n\t\t\treturn [];\n\t\t}\n\n\t\t// Check if bgFn output changed by sampling\n\t\tconst bgSample = this.bgFn ? this.bgFn(\"test\") : undefined;\n\n\t\t// Check cache validity\n\t\tif (this.matchCache(width, childLines, bgSample)) {\n\t\t\treturn this.cache!.lines;\n\t\t}\n\n\t\t// Apply background and padding\n\t\tconst result: string[] = [];\n\n\t\t// Top padding\n\t\tfor (let i = 0; i < this.paddingY; i++) {\n\t\t\tresult.push(this.applyBg(\"\", width));\n\t\t}\n\n\t\t// Content\n\t\tfor (const line of childLines) {\n\t\t\tresult.push(this.applyBg(line, width));\n\t\t}\n\n\t\t// Bottom padding\n\t\tfor (let i = 0; i < this.paddingY; i++) {\n\t\t\tresult.push(this.applyBg(\"\", width));\n\t\t}\n\n\t\t// Update cache\n\t\tthis.cache = { childLines, width, bgSample, lines: result };\n\n\t\treturn result;\n\t}\n\n\tprivate applyBg(line: string, width: number): string {\n\t\tconst visLen = visibleWidth(line);\n\t\tconst padNeeded = Math.max(0, width - visLen);\n\t\tconst padded = line + \" \".repeat(padNeeded);\n\n\t\tif (this.bgFn) {\n\t\t\treturn applyBackgroundToLine(padded, width, this.bgFn);\n\t\t}\n\t\treturn padded;\n\t}\n}\n"]}
@@ -31,6 +31,16 @@ export class Box implements Component {
31
31
  this.invalidateCache();
32
32
  }
33
33
 
34
+ insertChildBefore(component: Component, before: Component): void {
35
+ const index = this.children.indexOf(before);
36
+ if (index !== -1) {
37
+ this.children.splice(index, 0, component);
38
+ } else {
39
+ this.children.push(component);
40
+ }
41
+ this.invalidateCache();
42
+ }
43
+
34
44
  removeChild(component: Component): void {
35
45
  const index = this.children.indexOf(component);
36
46
  if (index !== -1) {
@@ -58,7 +58,7 @@ import { initRoutingHistory } from "./routing-history.js";
58
58
  import { restoreHookState, resetHookState } from "./post-unit-hooks.js";
59
59
  import { resetProactiveHealing, setLevelChangeCallback } from "./doctor-proactive.js";
60
60
  import { snapshotSkills } from "./skill-discovery.js";
61
- import { isDbAvailable } from "./gsd-db.js";
61
+ import { isDbAvailable, getMilestone } from "./gsd-db.js";
62
62
  import { hideFooter } from "./auto-dashboard.js";
63
63
  import {
64
64
  debugLog,
@@ -683,6 +683,12 @@ export async function bootstrapAutoSession(
683
683
  if (milestoneIds.length > 1) {
684
684
  const issues: string[] = [];
685
685
  for (const id of milestoneIds) {
686
+ // Skip completed/parked milestones — a leftover CONTEXT-DRAFT.md
687
+ // on a finished milestone is harmless residue, not an actionable warning.
688
+ if (isDbAvailable()) {
689
+ const ms = getMilestone(id);
690
+ if (ms?.status === "complete" || ms?.status === "parked") continue;
691
+ }
686
692
  const draft = resolveMilestoneFile(base, id, "CONTEXT-DRAFT");
687
693
  if (draft)
688
694
  issues.push(
@@ -37,7 +37,7 @@ import { ensurePreferencesFile, serializePreferencesToFrontmatter } from "./comm
37
37
  // ─── Types ────────────────────────────────────────────────────────────────────
38
38
 
39
39
  interface ForensicAnomaly {
40
- type: "stuck-loop" | "cost-spike" | "timeout" | "missing-artifact" | "crash" | "doctor-issue" | "error-trace";
40
+ type: "stuck-loop" | "cost-spike" | "timeout" | "missing-artifact" | "crash" | "doctor-issue" | "error-trace" | "journal-stuck" | "journal-guard-block" | "journal-rapid-iterations" | "journal-worktree-failure";
41
41
  severity: "info" | "warning" | "error";
42
42
  unitType?: string;
43
43
  unitId?: string;
@@ -54,6 +54,37 @@ interface UnitTrace {
54
54
  mtime: number;
55
55
  }
56
56
 
57
+ /** Summary of .gsd/activity/ directory metadata. */
58
+ interface ActivityLogMeta {
59
+ fileCount: number;
60
+ totalSizeBytes: number;
61
+ oldestFile: string | null;
62
+ newestFile: string | null;
63
+ }
64
+
65
+ /**
66
+ * Summary of .gsd/journal/ data for forensic investigation.
67
+ *
68
+ * To avoid loading huge journal histories into memory, only the most recent
69
+ * daily files are fully parsed. Older files are line-counted for totals.
70
+ * Event counts and flow IDs reflect only recent files.
71
+ */
72
+ interface JournalSummary {
73
+ /** Total journal entries across all files (recent parsed + older line-counted) */
74
+ totalEntries: number;
75
+ /** Distinct flow IDs from recent files (each = one auto-mode iteration) */
76
+ flowCount: number;
77
+ /** Event counts by type (from recent files only) */
78
+ eventCounts: Record<string, number>;
79
+ /** Most recent journal entries (last 20) for context */
80
+ recentEvents: { ts: string; flowId: string; eventType: string; rule?: string; unitId?: string }[];
81
+ /** Date range of journal data */
82
+ oldestEntry: string | null;
83
+ newestEntry: string | null;
84
+ /** Daily file count */
85
+ fileCount: number;
86
+ }
87
+
57
88
  interface ForensicReport {
58
89
  gsdVersion: string;
59
90
  timestamp: string;
@@ -68,6 +99,8 @@ interface ForensicReport {
68
99
  doctorIssues: DoctorIssue[];
69
100
  anomalies: ForensicAnomaly[];
70
101
  recentUnits: { type: string; id: string; cost: number; duration: number; model: string; finishedAt: number }[];
102
+ journalSummary: JournalSummary | null;
103
+ activityLogMeta: ActivityLogMeta | null;
71
104
  }
72
105
 
73
106
  // ─── Duplicate Detection ──────────────────────────────────────────────────────
@@ -276,7 +309,13 @@ export async function buildForensicReport(basePath: string): Promise<ForensicRep
276
309
  // from import.meta.url would resolve to ~/package.json (wrong on every system).
277
310
  const gsdVersion = process.env.GSD_VERSION || "unknown";
278
311
 
279
- // 9. Run anomaly detectors
312
+ // 9. Scan journal for flow timeline and structured events
313
+ const journalSummary = scanJournalForForensics(basePath);
314
+
315
+ // 10. Gather activity log directory metadata
316
+ const activityLogMeta = gatherActivityLogMeta(basePath, activeMilestone);
317
+
318
+ // 11. Run anomaly detectors
280
319
  if (metrics?.units) detectStuckLoops(metrics.units, anomalies);
281
320
  if (metrics?.units) detectCostSpikes(metrics.units, anomalies);
282
321
  detectTimeouts(unitTraces, anomalies);
@@ -284,6 +323,7 @@ export async function buildForensicReport(basePath: string): Promise<ForensicRep
284
323
  detectCrash(crashLock, anomalies);
285
324
  detectDoctorIssues(doctorIssues, anomalies);
286
325
  detectErrorTraces(unitTraces, anomalies);
326
+ detectJournalAnomalies(journalSummary, anomalies);
287
327
 
288
328
  return {
289
329
  gsdVersion,
@@ -299,6 +339,8 @@ export async function buildForensicReport(basePath: string): Promise<ForensicRep
299
339
  doctorIssues,
300
340
  anomalies,
301
341
  recentUnits,
342
+ journalSummary,
343
+ activityLogMeta,
302
344
  };
303
345
  }
304
346
 
@@ -306,6 +348,9 @@ export async function buildForensicReport(basePath: string): Promise<ForensicRep
306
348
 
307
349
  const ACTIVITY_FILENAME_RE = /^(\d+)-(.+?)-(.+)\.jsonl$/;
308
350
 
351
+ /** Threshold below which iteration cadence is considered rapid (thrashing). */
352
+ const RAPID_ITERATION_THRESHOLD_MS = 5000;
353
+
309
354
  function scanActivityLogs(basePath: string, activeMilestone?: string | null): UnitTrace[] {
310
355
  const activityDirs = resolveActivityDirs(basePath, activeMilestone);
311
356
  const allTraces: UnitTrace[] = [];
@@ -380,6 +425,154 @@ function resolveActivityDirs(basePath: string, activeMilestone?: string | null):
380
425
  return dirs;
381
426
  }
382
427
 
428
+ // ─── Journal Scanner ──────────────────────────────────────────────────────────
429
+
430
+ /**
431
+ * Max recent journal files to fully parse for event counts and recent events.
432
+ * Older files are line-counted only to avoid loading huge amounts of data.
433
+ */
434
+ const MAX_JOURNAL_RECENT_FILES = 3;
435
+
436
+ /** Max recent events to extract for the forensic report timeline. */
437
+ const MAX_JOURNAL_RECENT_EVENTS = 20;
438
+
439
+ /**
440
+ * Intelligently scan journal files for forensic summary.
441
+ *
442
+ * Journal files can be huge (thousands of JSONL entries over weeks of auto-mode).
443
+ * Instead of loading all entries into memory:
444
+ * - Only fully parse the most recent N daily files (event counts, flow tracking)
445
+ * - Line-count older files for approximate totals (no JSON parsing)
446
+ * - Extract only the last 20 events for the timeline
447
+ */
448
+ function scanJournalForForensics(basePath: string): JournalSummary | null {
449
+ try {
450
+ const journalDir = join(gsdRoot(basePath), "journal");
451
+ if (!existsSync(journalDir)) return null;
452
+
453
+ const files = readdirSync(journalDir).filter(f => f.endsWith(".jsonl")).sort();
454
+ if (files.length === 0) return null;
455
+
456
+ // Split into recent (fully parsed) and older (line-counted only)
457
+ const recentFiles = files.slice(-MAX_JOURNAL_RECENT_FILES);
458
+ const olderFiles = files.slice(0, -MAX_JOURNAL_RECENT_FILES);
459
+
460
+ // Line-count older files without parsing — avoids loading megabytes of JSON
461
+ let olderEntryCount = 0;
462
+ let oldestEntry: string | null = null;
463
+ for (const file of olderFiles) {
464
+ try {
465
+ const raw = readFileSync(join(journalDir, file), "utf-8");
466
+ const lines = raw.split("\n");
467
+ for (const line of lines) {
468
+ if (!line.trim()) continue;
469
+ olderEntryCount++;
470
+ // Extract only the timestamp from the first non-empty line of the oldest file
471
+ if (!oldestEntry) {
472
+ try {
473
+ const parsed = JSON.parse(line) as { ts?: string };
474
+ if (parsed.ts) oldestEntry = parsed.ts;
475
+ } catch { /* skip malformed */ }
476
+ }
477
+ }
478
+ } catch { /* skip unreadable files */ }
479
+ }
480
+
481
+ // Fully parse recent files for event counts and timeline
482
+ const eventCounts: Record<string, number> = {};
483
+ const flowIds = new Set<string>();
484
+ const recentParsedEntries: { ts: string; flowId: string; eventType: string; rule?: string; unitId?: string }[] = [];
485
+ let recentEntryCount = 0;
486
+
487
+ for (const file of recentFiles) {
488
+ try {
489
+ const raw = readFileSync(join(journalDir, file), "utf-8");
490
+ for (const line of raw.split("\n")) {
491
+ if (!line.trim()) continue;
492
+ try {
493
+ const entry = JSON.parse(line) as { ts: string; flowId: string; eventType: string; rule?: string; data?: Record<string, unknown> };
494
+ recentEntryCount++;
495
+ eventCounts[entry.eventType] = (eventCounts[entry.eventType] ?? 0) + 1;
496
+ flowIds.add(entry.flowId);
497
+
498
+ if (!oldestEntry) oldestEntry = entry.ts;
499
+
500
+ // Keep a rolling window of last N events — avoids accumulating unbounded arrays
501
+ recentParsedEntries.push({
502
+ ts: entry.ts,
503
+ flowId: entry.flowId,
504
+ eventType: entry.eventType,
505
+ rule: entry.rule,
506
+ unitId: entry.data?.unitId as string | undefined,
507
+ });
508
+ if (recentParsedEntries.length > MAX_JOURNAL_RECENT_EVENTS) {
509
+ recentParsedEntries.shift();
510
+ }
511
+ } catch { /* skip malformed lines */ }
512
+ }
513
+ } catch { /* skip unreadable files */ }
514
+ }
515
+
516
+ const totalEntries = olderEntryCount + recentEntryCount;
517
+ if (totalEntries === 0) return null;
518
+
519
+ const newestEntry = recentParsedEntries.length > 0
520
+ ? recentParsedEntries[recentParsedEntries.length - 1]!.ts
521
+ : null;
522
+
523
+ return {
524
+ totalEntries,
525
+ flowCount: flowIds.size,
526
+ eventCounts,
527
+ recentEvents: recentParsedEntries,
528
+ oldestEntry,
529
+ newestEntry,
530
+ fileCount: files.length,
531
+ };
532
+ } catch {
533
+ return null;
534
+ }
535
+ }
536
+
537
+ // ─── Activity Log Metadata ────────────────────────────────────────────────────
538
+
539
+ function gatherActivityLogMeta(basePath: string, activeMilestone?: string | null): ActivityLogMeta | null {
540
+ try {
541
+ const activityDirs = resolveActivityDirs(basePath, activeMilestone);
542
+ let fileCount = 0;
543
+ let totalSizeBytes = 0;
544
+ let oldestFile: string | null = null;
545
+ let newestFile: string | null = null;
546
+ let oldestMtime = Infinity;
547
+ let newestMtime = 0;
548
+
549
+ for (const activityDir of activityDirs) {
550
+ if (!existsSync(activityDir)) continue;
551
+ const files = readdirSync(activityDir).filter(f => f.endsWith(".jsonl"));
552
+ for (const file of files) {
553
+ const filePath = join(activityDir, file);
554
+ const stat = statSync(filePath, { throwIfNoEntry: false });
555
+ if (!stat) continue;
556
+ fileCount++;
557
+ totalSizeBytes += stat.size;
558
+ if (stat.mtimeMs < oldestMtime) {
559
+ oldestMtime = stat.mtimeMs;
560
+ oldestFile = file;
561
+ }
562
+ if (stat.mtimeMs > newestMtime) {
563
+ newestMtime = stat.mtimeMs;
564
+ newestFile = file;
565
+ }
566
+ }
567
+ }
568
+
569
+ if (fileCount === 0) return null;
570
+ return { fileCount, totalSizeBytes, oldestFile, newestFile };
571
+ } catch {
572
+ return null;
573
+ }
574
+ }
575
+
383
576
  // ─── Completed Keys Loader ────────────────────────────────────────────────────
384
577
 
385
578
  function loadCompletedKeys(basePath: string): string[] {
@@ -524,6 +717,66 @@ function detectErrorTraces(traces: UnitTrace[], anomalies: ForensicAnomaly[]): v
524
717
  }
525
718
  }
526
719
 
720
+ function detectJournalAnomalies(journal: JournalSummary | null, anomalies: ForensicAnomaly[]): void {
721
+ if (!journal) return;
722
+
723
+ // Detect stuck-detected events from the journal
724
+ const stuckCount = journal.eventCounts["stuck-detected"] ?? 0;
725
+ if (stuckCount > 0) {
726
+ anomalies.push({
727
+ type: "journal-stuck",
728
+ severity: stuckCount >= 3 ? "error" : "warning",
729
+ summary: `Journal recorded ${stuckCount} stuck-detected event(s)`,
730
+ details: `The auto-mode loop detected it was stuck ${stuckCount} time(s). Check journal events for flow IDs and causal chains to trace the root cause.`,
731
+ });
732
+ }
733
+
734
+ // Detect guard-block events (dispatch was blocked by a guard)
735
+ const guardCount = journal.eventCounts["guard-block"] ?? 0;
736
+ if (guardCount > 0) {
737
+ anomalies.push({
738
+ type: "journal-guard-block",
739
+ severity: guardCount >= 5 ? "warning" : "info",
740
+ summary: `Journal recorded ${guardCount} guard-block event(s)`,
741
+ details: `Dispatch was blocked by a guard condition ${guardCount} time(s). This may indicate a persistent blocking condition preventing progress.`,
742
+ });
743
+ }
744
+
745
+ // Detect rapid iterations (many flows in short time = likely thrashing)
746
+ if (journal.flowCount > 0 && journal.oldestEntry && journal.newestEntry) {
747
+ const oldest = new Date(journal.oldestEntry).getTime();
748
+ const newest = new Date(journal.newestEntry).getTime();
749
+ const spanMs = newest - oldest;
750
+ if (spanMs > 0 && journal.flowCount > 10) {
751
+ const avgMs = spanMs / journal.flowCount;
752
+ if (avgMs < RAPID_ITERATION_THRESHOLD_MS) {
753
+ anomalies.push({
754
+ type: "journal-rapid-iterations",
755
+ severity: "warning",
756
+ summary: `${journal.flowCount} iterations in ${formatDuration(spanMs)} (avg ${formatDuration(avgMs)}/iteration)`,
757
+ details: `Unusually rapid iteration cadence suggests the loop may be thrashing without making progress. Review recent journal events for dispatch-stop or terminal events.`,
758
+ });
759
+ }
760
+ }
761
+ }
762
+
763
+ // Detect worktree failures from journal events
764
+ const wtCreateFailed = journal.eventCounts["worktree-create-failed"] ?? 0;
765
+ const wtMergeFailed = journal.eventCounts["worktree-merge-failed"] ?? 0;
766
+ const wtFailures = wtCreateFailed + wtMergeFailed;
767
+ if (wtFailures > 0) {
768
+ const parts: string[] = [];
769
+ if (wtCreateFailed > 0) parts.push(`${wtCreateFailed} create failure(s)`);
770
+ if (wtMergeFailed > 0) parts.push(`${wtMergeFailed} merge failure(s)`);
771
+ anomalies.push({
772
+ type: "journal-worktree-failure",
773
+ severity: "warning",
774
+ summary: `Worktree failures: ${parts.join(", ")}`,
775
+ details: `Journal recorded worktree operation failures. These may indicate git state corruption or conflicting branches.`,
776
+ });
777
+ }
778
+ }
779
+
527
780
  // ─── Report Persistence ───────────────────────────────────────────────────────
528
781
 
529
782
  function saveForensicReport(basePath: string, report: ForensicReport, problemDescription: string): string {
@@ -600,6 +853,45 @@ function saveForensicReport(basePath: string, report: ForensicReport, problemDes
600
853
  sections.push(redact(formatCrashInfo(report.crashLock)), ``);
601
854
  }
602
855
 
856
+ // Activity log metadata
857
+ if (report.activityLogMeta) {
858
+ const meta = report.activityLogMeta;
859
+ sections.push(`## Activity Log Metadata`, ``);
860
+ sections.push(`- Files: ${meta.fileCount}`);
861
+ sections.push(`- Total size: ${(meta.totalSizeBytes / 1024).toFixed(1)} KB`);
862
+ if (meta.oldestFile) sections.push(`- Oldest: ${meta.oldestFile}`);
863
+ if (meta.newestFile) sections.push(`- Newest: ${meta.newestFile}`);
864
+ sections.push(``);
865
+ }
866
+
867
+ // Journal summary
868
+ if (report.journalSummary) {
869
+ const js = report.journalSummary;
870
+ sections.push(`## Journal Summary`, ``);
871
+ sections.push(`- Total entries: ${js.totalEntries}`);
872
+ sections.push(`- Distinct flows (iterations): ${js.flowCount}`);
873
+ sections.push(`- Daily files: ${js.fileCount}`);
874
+ if (js.oldestEntry) sections.push(`- Date range: ${js.oldestEntry} — ${js.newestEntry}`);
875
+ sections.push(``);
876
+ sections.push(`### Event Type Distribution`, ``);
877
+ sections.push(`| Event Type | Count |`);
878
+ sections.push(`|------------|-------|`);
879
+ for (const [evType, count] of Object.entries(js.eventCounts).sort((a, b) => b[1] - a[1])) {
880
+ sections.push(`| ${evType} | ${count} |`);
881
+ }
882
+ sections.push(``);
883
+ if (js.recentEvents.length > 0) {
884
+ sections.push(`### Recent Journal Events (last ${js.recentEvents.length})`, ``);
885
+ for (const ev of js.recentEvents) {
886
+ const parts = [`${ev.ts} [${ev.eventType}] flow=${ev.flowId.slice(0, 8)}`];
887
+ if (ev.rule) parts.push(`rule=${ev.rule}`);
888
+ if (ev.unitId) parts.push(`unit=${ev.unitId}`);
889
+ sections.push(`- ${parts.join(" ")}`);
890
+ }
891
+ sections.push(``);
892
+ }
893
+ }
894
+
603
895
  writeFileSync(filePath, sections.join("\n"), "utf-8");
604
896
  return filePath;
605
897
  }
@@ -681,6 +973,41 @@ function formatReportForPrompt(report: ForensicReport): string {
681
973
  sections.push("");
682
974
  }
683
975
 
976
+ // Activity log metadata
977
+ if (report.activityLogMeta) {
978
+ const meta = report.activityLogMeta;
979
+ sections.push("### Activity Log Overview");
980
+ sections.push(`- Files: ${meta.fileCount}, Total size: ${(meta.totalSizeBytes / 1024).toFixed(1)} KB`);
981
+ if (meta.oldestFile) sections.push(`- Oldest: ${meta.oldestFile}`);
982
+ if (meta.newestFile) sections.push(`- Newest: ${meta.newestFile}`);
983
+ sections.push("");
984
+ }
985
+
986
+ // Journal summary — structured event timeline
987
+ if (report.journalSummary) {
988
+ const js = report.journalSummary;
989
+ sections.push("### Journal Summary (Iteration Event Log)");
990
+ sections.push(`- Total entries: ${js.totalEntries}, Distinct flows: ${js.flowCount}, Daily files: ${js.fileCount}`);
991
+ if (js.oldestEntry) sections.push(`- Date range: ${js.oldestEntry} — ${js.newestEntry}`);
992
+
993
+ // Event type distribution (compact)
994
+ const eventPairs = Object.entries(js.eventCounts).sort((a, b) => b[1] - a[1]);
995
+ sections.push(`- Events: ${eventPairs.map(([t, c]) => `${t}(${c})`).join(", ")}`);
996
+
997
+ // Recent events timeline (for tracing what just happened)
998
+ if (js.recentEvents.length > 0) {
999
+ sections.push("");
1000
+ sections.push(`**Recent Journal Events (last ${js.recentEvents.length}):**`);
1001
+ for (const ev of js.recentEvents) {
1002
+ const parts = [`${ev.ts} [${ev.eventType}] flow=${ev.flowId.slice(0, 8)}`];
1003
+ if (ev.rule) parts.push(`rule=${ev.rule}`);
1004
+ if (ev.unitId) parts.push(`unit=${ev.unitId}`);
1005
+ sections.push(`- ${parts.join(" ")}`);
1006
+ }
1007
+ }
1008
+ sections.push("");
1009
+ }
1010
+
684
1011
  // Completed keys count
685
1012
  sections.push(`### Completed Keys: ${report.completedKeys.length}`);
686
1013
  sections.push(`### GSD Version: ${report.gsdVersion}`);