@opengsd/gsd-pi 1.1.1-dev.9f86580 → 1.1.1-dev.b2556262
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/headless-recover.js +56 -1
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/browser-tools/index.js +39 -22
- package/dist/resources/extensions/browser-tools/state.js +12 -0
- package/dist/resources/extensions/browser-tools/tools/session.js +3 -2
- package/dist/resources/extensions/browser-tools/utils.js +3 -3
- package/dist/resources/extensions/gsd/auto/loop.js +4 -2
- package/dist/resources/extensions/gsd/auto/phases.js +43 -10
- package/dist/resources/extensions/gsd/auto/session.js +20 -1
- package/dist/resources/extensions/gsd/auto/workflow-kernel.js +1 -0
- package/dist/resources/extensions/gsd/auto-dispatch.js +72 -12
- package/dist/resources/extensions/gsd/auto-model-selection.js +128 -9
- package/dist/resources/extensions/gsd/auto-post-unit.js +19 -2
- package/dist/resources/extensions/gsd/auto-prompts.js +24 -19
- package/dist/resources/extensions/gsd/auto-recovery.js +4 -2
- package/dist/resources/extensions/gsd/auto-runtime-state.js +3 -0
- package/dist/resources/extensions/gsd/auto-start.js +1 -1
- package/dist/resources/extensions/gsd/auto.js +14 -11
- package/dist/resources/extensions/gsd/bootstrap/db-tools.js +3 -3
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +172 -65
- package/dist/resources/extensions/gsd/closeout-wizard.js +32 -9
- package/dist/resources/extensions/gsd/commands/handlers/ops.js +2 -9
- package/dist/resources/extensions/gsd/commands-maintenance.js +93 -15
- package/dist/resources/extensions/gsd/commands-prefs-wizard.js +2 -2
- package/dist/resources/extensions/gsd/db-writer.js +35 -0
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +50 -1
- package/dist/resources/extensions/gsd/gsd-db.js +480 -172
- package/dist/resources/extensions/gsd/markdown-renderer.js +37 -53
- package/dist/resources/extensions/gsd/md-importer.js +38 -3
- package/dist/resources/extensions/gsd/migration-auto-check.js +126 -31
- package/dist/resources/extensions/gsd/parsers-legacy.js +23 -0
- package/dist/resources/extensions/gsd/planning-path-scope.js +22 -4
- package/dist/resources/extensions/gsd/pre-execution-checks.js +10 -2
- package/dist/resources/extensions/gsd/preferences-models.js +110 -43
- package/dist/resources/extensions/gsd/preferences-types.js +13 -0
- package/dist/resources/extensions/gsd/preferences-validation.js +68 -3
- package/dist/resources/extensions/gsd/preferences.js +4 -1
- package/dist/resources/extensions/gsd/prompts/gate-evaluate.md +1 -1
- package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/refine-slice.md +1 -1
- package/dist/resources/extensions/gsd/roadmap-slices.js +5 -1
- package/dist/resources/extensions/gsd/safety/content-validator.js +6 -4
- package/dist/resources/extensions/gsd/source-observations.js +306 -0
- package/dist/resources/extensions/gsd/state-reconciliation/drift/completion.js +15 -8
- package/dist/resources/extensions/gsd/state-reconciliation/drift/stale-render.js +33 -5
- package/dist/resources/extensions/gsd/state-reconciliation/drift/stale-worker.js +34 -13
- package/dist/resources/extensions/gsd/state-reconciliation/index.js +39 -14
- package/dist/resources/extensions/gsd/state-reconciliation/spawn-gate.js +4 -4
- package/dist/resources/extensions/gsd/state.js +7 -3
- package/dist/resources/extensions/gsd/tool-contract.js +14 -0
- package/dist/resources/extensions/gsd/tool-presentation-plan.js +1 -9
- package/dist/resources/extensions/gsd/tools/complete-slice.js +7 -6
- package/dist/resources/extensions/gsd/tools/plan-slice.js +42 -11
- package/dist/resources/extensions/gsd/tools/plan-task.js +7 -1
- package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +57 -429
- package/dist/resources/extensions/gsd/uat-policy.js +130 -0
- package/dist/resources/extensions/gsd/uat-run.js +414 -0
- package/dist/resources/extensions/gsd/unit-context-manifest.js +3 -4
- package/dist/resources/extensions/gsd/verdict-parser.js +3 -8
- package/dist/resources/extensions/gsd/workflow-manifest.js +132 -5
- package/dist/resources/extensions/gsd/workflow-projections.js +8 -0
- package/dist/resources/extensions/gsd/worktree-state-projection.js +18 -17
- package/dist/resources/extensions/subagent/agents.js +1 -0
- package/dist/resources/extensions/subagent/index.js +27 -12
- package/dist/resources/extensions/subagent/launch.js +7 -2
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +8 -8
- 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 +8 -8
- package/dist/web/standalone/.next/server/chunks/8357.js +1 -1
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/dist/web/standalone/node_modules/@gsd/native/dist/native.js +22 -0
- package/dist/web/standalone/node_modules/node-pty/build/Makefile +1 -1
- package/package.json +4 -4
- package/packages/cloud-mcp-gateway/package.json +2 -2
- package/packages/contracts/package.json +1 -1
- package/packages/daemon/package.json +4 -4
- package/packages/gsd-agent-core/package.json +5 -5
- package/packages/gsd-agent-modes/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/assistant-message.js +21 -23
- package/packages/gsd-agent-modes/dist/modes/interactive/components/assistant-message.js.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.d.ts +3 -0
- package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.js +25 -0
- package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.d.ts +1 -0
- package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.js +66 -12
- package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.js.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.js +18 -11
- package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-chat-render.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-chat-render.js +16 -0
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-chat-render.js.map +1 -1
- package/packages/gsd-agent-modes/package.json +7 -7
- package/packages/mcp-server/dist/workflow-tools.js +1 -1
- package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
- package/packages/mcp-server/package.json +3 -3
- package/packages/native/dist/native.js +22 -0
- package/packages/native/package.json +1 -1
- package/packages/pi-agent-core/package.json +1 -1
- package/packages/pi-ai/dist/image-models.generated.d.ts +30 -0
- package/packages/pi-ai/dist/image-models.generated.d.ts.map +1 -1
- package/packages/pi-ai/dist/image-models.generated.js +30 -0
- package/packages/pi-ai/dist/image-models.generated.js.map +1 -1
- package/packages/pi-ai/dist/models.generated.d.ts +23 -17
- package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
- package/packages/pi-ai/dist/models.generated.js +25 -24
- package/packages/pi-ai/dist/models.generated.js.map +1 -1
- package/packages/pi-ai/package.json +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.js +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/theme/themes.js +1 -1
- package/packages/pi-coding-agent/dist/theme/themes.js.map +1 -1
- package/packages/pi-coding-agent/package.json +7 -7
- package/packages/pi-tui/dist/utils.d.ts +11 -0
- package/packages/pi-tui/dist/utils.d.ts.map +1 -1
- package/packages/pi-tui/dist/utils.js +119 -6
- package/packages/pi-tui/dist/utils.js.map +1 -1
- package/packages/pi-tui/package.json +2 -1
- package/packages/rpc-client/package.json +2 -2
- package/pkg/dist/theme/themes.js +1 -1
- package/pkg/dist/theme/themes.js.map +1 -1
- package/pkg/package.json +1 -1
- package/src/resources/extensions/browser-tools/index.ts +39 -22
- package/src/resources/extensions/browser-tools/state.ts +13 -0
- package/src/resources/extensions/browser-tools/tests/browser-tools-unit.test.cjs +57 -0
- package/src/resources/extensions/browser-tools/tools/session.ts +4 -2
- package/src/resources/extensions/browser-tools/utils.ts +3 -3
- package/src/resources/extensions/gsd/auto/loop-deps.ts +1 -0
- package/src/resources/extensions/gsd/auto/loop.ts +4 -2
- package/src/resources/extensions/gsd/auto/phases.ts +42 -10
- package/src/resources/extensions/gsd/auto/session.ts +22 -1
- package/src/resources/extensions/gsd/auto/workflow-kernel.ts +1 -0
- package/src/resources/extensions/gsd/auto-dispatch.ts +85 -12
- package/src/resources/extensions/gsd/auto-model-selection.ts +164 -12
- package/src/resources/extensions/gsd/auto-post-unit.ts +20 -2
- package/src/resources/extensions/gsd/auto-prompts.ts +23 -20
- package/src/resources/extensions/gsd/auto-recovery.ts +22 -3
- package/src/resources/extensions/gsd/auto-runtime-state.ts +5 -0
- package/src/resources/extensions/gsd/auto-start.ts +1 -1
- package/src/resources/extensions/gsd/auto.ts +13 -10
- package/src/resources/extensions/gsd/bootstrap/db-tools.ts +3 -3
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +225 -72
- package/src/resources/extensions/gsd/closeout-wizard.ts +47 -13
- package/src/resources/extensions/gsd/commands/handlers/ops.ts +2 -17
- package/src/resources/extensions/gsd/commands-maintenance.ts +124 -13
- package/src/resources/extensions/gsd/commands-prefs-wizard.ts +2 -2
- package/src/resources/extensions/gsd/db-writer.ts +38 -0
- package/src/resources/extensions/gsd/docs/preferences-reference.md +50 -1
- package/src/resources/extensions/gsd/gsd-db.ts +564 -186
- package/src/resources/extensions/gsd/markdown-renderer.ts +44 -66
- package/src/resources/extensions/gsd/md-importer.ts +49 -2
- package/src/resources/extensions/gsd/migration-auto-check.ts +154 -34
- package/src/resources/extensions/gsd/parsers-legacy.ts +20 -0
- package/src/resources/extensions/gsd/planning-path-scope.ts +22 -4
- package/src/resources/extensions/gsd/pre-execution-checks.ts +9 -2
- package/src/resources/extensions/gsd/preferences-models.ts +112 -43
- package/src/resources/extensions/gsd/preferences-types.ts +39 -0
- package/src/resources/extensions/gsd/preferences-validation.ts +76 -2
- package/src/resources/extensions/gsd/preferences.ts +5 -0
- package/src/resources/extensions/gsd/prompts/gate-evaluate.md +1 -1
- package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/refine-slice.md +1 -1
- package/src/resources/extensions/gsd/roadmap-slices.ts +6 -1
- package/src/resources/extensions/gsd/safety/content-validator.ts +8 -5
- package/src/resources/extensions/gsd/source-observations.ts +402 -0
- package/src/resources/extensions/gsd/state-reconciliation/drift/completion.ts +20 -8
- package/src/resources/extensions/gsd/state-reconciliation/drift/stale-render.ts +44 -5
- package/src/resources/extensions/gsd/state-reconciliation/drift/stale-worker.ts +39 -11
- package/src/resources/extensions/gsd/state-reconciliation/index.ts +45 -15
- package/src/resources/extensions/gsd/state-reconciliation/spawn-gate.ts +4 -4
- package/src/resources/extensions/gsd/state.ts +7 -4
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +15 -0
- package/src/resources/extensions/gsd/tests/auto-model-selection.test.ts +299 -1
- package/src/resources/extensions/gsd/tests/auto-paused-ui-cleanup.test.ts +32 -0
- package/src/resources/extensions/gsd/tests/auto-phases-lifecycle.test.ts +75 -3
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +22 -1
- package/src/resources/extensions/gsd/tests/before-provider-context-management.test.ts +145 -0
- package/src/resources/extensions/gsd/tests/closeout-wizard.test.ts +44 -0
- package/src/resources/extensions/gsd/tests/commands-dispatcher-unmerged-milestone.test.ts +26 -1
- package/src/resources/extensions/gsd/tests/content-validator.test.ts +74 -0
- package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +16 -2
- package/src/resources/extensions/gsd/tests/doctor-scope-db-unavailable.test.ts +1 -11
- package/src/resources/extensions/gsd/tests/gate-dispatch.test.ts +64 -0
- package/src/resources/extensions/gsd/tests/gate-storage.test.ts +15 -0
- package/src/resources/extensions/gsd/tests/gsd-recover.test.ts +62 -1
- package/src/resources/extensions/gsd/tests/journal-integration.test.ts +15 -0
- package/src/resources/extensions/gsd/tests/markdown-renderer.test.ts +42 -0
- package/src/resources/extensions/gsd/tests/migration-auto-check.test.ts +99 -0
- package/src/resources/extensions/gsd/tests/plan-slice.test.ts +99 -2
- package/src/resources/extensions/gsd/tests/plan-task.test.ts +19 -0
- package/src/resources/extensions/gsd/tests/preferences.test.ts +14 -0
- package/src/resources/extensions/gsd/tests/prefs-wizard-coverage.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +9 -0
- package/src/resources/extensions/gsd/tests/register-hooks-depth-verification.test.ts +101 -1
- package/src/resources/extensions/gsd/tests/repository-registry.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/runtime-invariant-modules.test.ts +8 -0
- package/src/resources/extensions/gsd/tests/schema-v21-sequence.test.ts +5 -3
- package/src/resources/extensions/gsd/tests/schema-v27-v28-sequence.test.ts +162 -18
- package/src/resources/extensions/gsd/tests/skipped-validation-db-atomicity.test.ts +8 -0
- package/src/resources/extensions/gsd/tests/source-observations.test.ts +275 -0
- package/src/resources/extensions/gsd/tests/stale-queued-milestone.test.ts +43 -0
- package/src/resources/extensions/gsd/tests/state-reconciliation-drift.test.ts +76 -21
- package/src/resources/extensions/gsd/tests/thinking-level-resolution.test.ts +203 -0
- package/src/resources/extensions/gsd/tests/uat-policy.test.ts +170 -0
- package/src/resources/extensions/gsd/tests/unit-context-manifest.test.ts +7 -1
- package/src/resources/extensions/gsd/tests/workflow-kernel.test.ts +7 -0
- package/src/resources/extensions/gsd/tests/workflow-manifest.test.ts +306 -1
- package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +73 -6
- package/src/resources/extensions/gsd/tests/worktree-db.test.ts +511 -1
- package/src/resources/extensions/gsd/tests/worktree-state-projection.test.ts +44 -0
- package/src/resources/extensions/gsd/tool-contract.ts +28 -0
- package/src/resources/extensions/gsd/tool-presentation-plan.ts +1 -11
- package/src/resources/extensions/gsd/tools/complete-slice.ts +7 -6
- package/src/resources/extensions/gsd/tools/plan-slice.ts +54 -12
- package/src/resources/extensions/gsd/tools/plan-task.ts +8 -1
- package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +66 -526
- package/src/resources/extensions/gsd/types.ts +1 -0
- package/src/resources/extensions/gsd/uat-policy.ts +191 -0
- package/src/resources/extensions/gsd/uat-run.ts +550 -0
- package/src/resources/extensions/gsd/unit-context-manifest.ts +3 -4
- package/src/resources/extensions/gsd/verdict-parser.ts +3 -10
- package/src/resources/extensions/gsd/workflow-manifest.ts +193 -7
- package/src/resources/extensions/gsd/workflow-projections.ts +9 -0
- package/src/resources/extensions/gsd/worktree-state-projection.ts +22 -22
- package/src/resources/extensions/shared/tests/format-utils.test.ts +8 -3
- package/src/resources/extensions/subagent/agents.ts +4 -0
- package/src/resources/extensions/subagent/index.ts +28 -3
- package/src/resources/extensions/subagent/launch.ts +8 -0
- package/src/resources/extensions/subagent/tests/model-override.test.ts +31 -0
- /package/dist/web/standalone/.next/static/{zzYMrKpPGfRQRxSFO32Jr → tJOKQbQRO-9MiFDO8DIDS}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{zzYMrKpPGfRQRxSFO32Jr → tJOKQbQRO-9MiFDO8DIDS}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
// Project/App: gsd-pi
|
|
2
|
+
// File Purpose: Active-unit source observations for provider payload context.
|
|
3
|
+
|
|
4
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
5
|
+
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
6
|
+
|
|
7
|
+
import type { TaskRow } from "./db-task-slice-rows.js";
|
|
8
|
+
import { extractPlanningPathReference, normalizeFilePath } from "./pre-execution-checks.js";
|
|
9
|
+
|
|
10
|
+
export const WHOLE_FILE_OBSERVATION_MAX_BYTES = 50 * 1024;
|
|
11
|
+
export const WHOLE_FILE_OBSERVATION_MAX_LINES = 2000;
|
|
12
|
+
|
|
13
|
+
export type SourceObservationStatus =
|
|
14
|
+
| "whole"
|
|
15
|
+
| "missing"
|
|
16
|
+
| "binary/image"
|
|
17
|
+
| "over-threshold"
|
|
18
|
+
| "glob"
|
|
19
|
+
| "directory"
|
|
20
|
+
| "unresolved selector";
|
|
21
|
+
|
|
22
|
+
export interface SourceObservationUnit {
|
|
23
|
+
unitType: string;
|
|
24
|
+
unitId: string;
|
|
25
|
+
startedAt: number;
|
|
26
|
+
basePath: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type SourceObservationSource = "plan" | "read" | "mutation";
|
|
30
|
+
|
|
31
|
+
export interface SourceObservation {
|
|
32
|
+
path: string;
|
|
33
|
+
absolutePath: string | null;
|
|
34
|
+
status: SourceObservationStatus;
|
|
35
|
+
source: SourceObservationSource;
|
|
36
|
+
text?: string;
|
|
37
|
+
bytes?: number;
|
|
38
|
+
lines?: number;
|
|
39
|
+
reason?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface ActiveSourceObservationSet {
|
|
43
|
+
unit: SourceObservationUnit;
|
|
44
|
+
observations: Map<string, SourceObservation>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const SOURCE_CONTEXT_TITLE = "## Source Context Block";
|
|
48
|
+
const SOURCE_OBSERVATION_UNIT_TYPE = "execute-task";
|
|
49
|
+
|
|
50
|
+
export function supportsSourceObservationsForUnit(unitType: string): boolean {
|
|
51
|
+
return unitType === SOURCE_OBSERVATION_UNIT_TYPE;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export class SourceObservationStore {
|
|
55
|
+
private active: ActiveSourceObservationSet | null = null;
|
|
56
|
+
|
|
57
|
+
beginUnit(unit: SourceObservationUnit): void {
|
|
58
|
+
if (!supportsSourceObservationsForUnit(unit.unitType)) {
|
|
59
|
+
this.clear();
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (this.matches(unit)) return;
|
|
63
|
+
this.active = { unit: { ...unit }, observations: new Map() };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
clear(): void {
|
|
67
|
+
this.active = null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
degradeUnit(unit: Pick<SourceObservationUnit, "unitType" | "unitId" | "startedAt">): void {
|
|
71
|
+
if (!this.active) return;
|
|
72
|
+
const current = this.active.unit;
|
|
73
|
+
if (
|
|
74
|
+
current.unitType === unit.unitType &&
|
|
75
|
+
current.unitId === unit.unitId &&
|
|
76
|
+
current.startedAt === unit.startedAt
|
|
77
|
+
) {
|
|
78
|
+
this.active = null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
observePlanTask(task: TaskRow): void {
|
|
83
|
+
if (!this.active) return;
|
|
84
|
+
for (const entry of planDeclaredSourceEntries(task)) {
|
|
85
|
+
this.observePath(entry.path, "plan");
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
observeRead(input: { path?: unknown; file_path?: unknown; [key: string]: unknown }): void {
|
|
90
|
+
if (!this.active) return;
|
|
91
|
+
const rawPath = readPathFromInput(input);
|
|
92
|
+
if (!rawPath.trim()) return;
|
|
93
|
+
this.observePath(rawPath, "read");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
observeMutation(input: { path?: unknown; file_path?: unknown; [key: string]: unknown }): void {
|
|
97
|
+
if (!this.active) return;
|
|
98
|
+
const rawPath = readPathFromInput(input);
|
|
99
|
+
if (!rawPath.trim()) return;
|
|
100
|
+
this.observePath(rawPath, "mutation", { replaceExisting: true });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
renderActiveBlock(): string | null {
|
|
104
|
+
if (!this.active || this.active.observations.size === 0) return null;
|
|
105
|
+
if (!supportsSourceObservationsForUnit(this.active.unit.unitType)) return null;
|
|
106
|
+
const observations = [...this.active.observations.values()]
|
|
107
|
+
.sort((a, b) => a.path.localeCompare(b.path));
|
|
108
|
+
const lines = [
|
|
109
|
+
SOURCE_CONTEXT_TITLE,
|
|
110
|
+
`Active Unit: ${this.active.unit.unitType} ${this.active.unit.unitId}`,
|
|
111
|
+
"",
|
|
112
|
+
"The files below are protected active-Unit source context. " +
|
|
113
|
+
"Use this block instead of rereading small files just to recover line windows.",
|
|
114
|
+
];
|
|
115
|
+
|
|
116
|
+
const wholeFiles = observations.filter((observation) => observation.status === "whole");
|
|
117
|
+
const unavailable = observations.filter((observation) => observation.status !== "whole");
|
|
118
|
+
|
|
119
|
+
if (wholeFiles.length > 0) {
|
|
120
|
+
lines.push("", "### Whole-File Observations");
|
|
121
|
+
for (const observation of wholeFiles) {
|
|
122
|
+
lines.push(
|
|
123
|
+
"",
|
|
124
|
+
`#### ${observation.path}`,
|
|
125
|
+
`Status: whole-file (${observation.lines ?? 0} lines, ${formatSize(observation.bytes ?? 0)})`,
|
|
126
|
+
fencedSource(observation.text ?? ""),
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (unavailable.length > 0) {
|
|
132
|
+
lines.push("", "### Unavailable Source Observations");
|
|
133
|
+
for (const observation of unavailable) {
|
|
134
|
+
const detail = observation.reason ? ` - ${observation.reason}` : "";
|
|
135
|
+
lines.push(`- ${observation.path}: ${observation.status}${detail}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return lines.join("\n");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private matches(unit: SourceObservationUnit): boolean {
|
|
143
|
+
if (!this.active) return false;
|
|
144
|
+
const current = this.active.unit;
|
|
145
|
+
return current.unitType === unit.unitType &&
|
|
146
|
+
current.unitId === unit.unitId &&
|
|
147
|
+
current.startedAt === unit.startedAt &&
|
|
148
|
+
current.basePath === unit.basePath;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private observePath(
|
|
152
|
+
rawPath: string,
|
|
153
|
+
source: SourceObservationSource,
|
|
154
|
+
options: { replaceExisting?: boolean } = {},
|
|
155
|
+
): void {
|
|
156
|
+
if (!this.active) return;
|
|
157
|
+
const observation = observeSourcePath(this.active.unit.basePath, rawPath, source);
|
|
158
|
+
const key = observation.absolutePath ?? `${observation.status}:${observation.path}`;
|
|
159
|
+
const existing = this.active.observations.get(key);
|
|
160
|
+
if (options.replaceExisting || !existing || observation.status === "whole" || existing.status !== "whole") {
|
|
161
|
+
this.active.observations.set(key, observation);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function planDeclaredSourceEntries(
|
|
167
|
+
task: TaskRow,
|
|
168
|
+
): Array<{ path: string; field: "files" | "inputs" }> {
|
|
169
|
+
const entries: Array<{ path: string; field: "files" | "inputs" }> = [];
|
|
170
|
+
for (const file of task.files) {
|
|
171
|
+
const path = extractPlanningPathReference(file) ?? file.trim();
|
|
172
|
+
if (path) entries.push({ path, field: "files" });
|
|
173
|
+
}
|
|
174
|
+
for (const input of task.inputs) {
|
|
175
|
+
const path = extractPlanningPathReference(input);
|
|
176
|
+
if (path) entries.push({ path, field: "inputs" });
|
|
177
|
+
}
|
|
178
|
+
return entries;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function observeSourcePath(
|
|
182
|
+
basePath: string,
|
|
183
|
+
rawPath: string,
|
|
184
|
+
source: SourceObservationSource,
|
|
185
|
+
): SourceObservation {
|
|
186
|
+
const normalizedRaw = normalizeFilePath(rawPath.trim());
|
|
187
|
+
const displayPath = normalizedRaw || rawPath.trim();
|
|
188
|
+
if (!normalizedRaw) {
|
|
189
|
+
return unavailable(displayPath || "<empty>", null, "unresolved selector", source);
|
|
190
|
+
}
|
|
191
|
+
if (containsGlobPattern(normalizedRaw)) {
|
|
192
|
+
return unavailable(displayPath, null, "glob", source, "glob selectors are not whole files");
|
|
193
|
+
}
|
|
194
|
+
if (rawPath.trim().endsWith("/")) {
|
|
195
|
+
return unavailable(displayPath, null, "directory", source, "directory selectors are not whole files");
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const rootPath = resolve(basePath);
|
|
199
|
+
const absolutePath = isAbsolute(normalizedRaw) ? resolve(normalizedRaw) : resolve(rootPath, normalizedRaw);
|
|
200
|
+
const path = formatObservationPath(rootPath, absolutePath, displayPath);
|
|
201
|
+
|
|
202
|
+
if (!isPathInsideRoot(rootPath, absolutePath)) {
|
|
203
|
+
return unavailable(displayPath, absolutePath, "unresolved selector", source, "path is outside active Unit root");
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (!existsSync(absolutePath)) {
|
|
207
|
+
return unavailable(path, absolutePath, "missing", source, "file does not exist in the active Unit root");
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
let stat;
|
|
211
|
+
try {
|
|
212
|
+
stat = statSync(absolutePath);
|
|
213
|
+
} catch {
|
|
214
|
+
return unavailable(path, absolutePath, "missing", source, "file could not be inspected");
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (stat.isDirectory()) {
|
|
218
|
+
return unavailable(path, absolutePath, "directory", source, "directory selectors are not whole files");
|
|
219
|
+
}
|
|
220
|
+
if (!stat.isFile()) {
|
|
221
|
+
return unavailable(path, absolutePath, "unresolved selector", source, "path is not a regular file");
|
|
222
|
+
}
|
|
223
|
+
if (stat.size > WHOLE_FILE_OBSERVATION_MAX_BYTES) {
|
|
224
|
+
return unavailable(
|
|
225
|
+
path,
|
|
226
|
+
absolutePath,
|
|
227
|
+
"over-threshold",
|
|
228
|
+
source,
|
|
229
|
+
`${formatSize(stat.size)} exceeds ${formatSize(WHOLE_FILE_OBSERVATION_MAX_BYTES)}`,
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
let buffer: Buffer;
|
|
234
|
+
try {
|
|
235
|
+
buffer = readFileSync(absolutePath);
|
|
236
|
+
} catch {
|
|
237
|
+
return unavailable(path, absolutePath, "missing", source, "file could not be read");
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (isBinaryOrImage(buffer)) {
|
|
241
|
+
return unavailable(path, absolutePath, "binary/image", source, "binary or image files are not inlined as source text");
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const text = buffer.toString("utf8");
|
|
245
|
+
const lines = countLines(text);
|
|
246
|
+
if (lines > WHOLE_FILE_OBSERVATION_MAX_LINES) {
|
|
247
|
+
return unavailable(
|
|
248
|
+
path,
|
|
249
|
+
absolutePath,
|
|
250
|
+
"over-threshold",
|
|
251
|
+
source,
|
|
252
|
+
`${lines} lines exceeds ${WHOLE_FILE_OBSERVATION_MAX_LINES}`,
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
path,
|
|
258
|
+
absolutePath,
|
|
259
|
+
status: "whole",
|
|
260
|
+
source,
|
|
261
|
+
text,
|
|
262
|
+
bytes: buffer.byteLength,
|
|
263
|
+
lines,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function injectSourceContextBlockIntoPayload(
|
|
268
|
+
payload: Record<string, unknown>,
|
|
269
|
+
block: string,
|
|
270
|
+
): Record<string, unknown> {
|
|
271
|
+
const messages = payload.messages;
|
|
272
|
+
if (Array.isArray(messages)) {
|
|
273
|
+
return {
|
|
274
|
+
...payload,
|
|
275
|
+
messages: [
|
|
276
|
+
...withoutExistingSourceContextMessages(messages),
|
|
277
|
+
{ role: "user", content: [{ type: "text", text: block }] },
|
|
278
|
+
],
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const input = payload.input;
|
|
283
|
+
if (Array.isArray(input)) {
|
|
284
|
+
return {
|
|
285
|
+
...payload,
|
|
286
|
+
input: [
|
|
287
|
+
...withoutExistingSourceContextItems(input),
|
|
288
|
+
{ role: "user", content: [{ type: "input_text", text: block }] },
|
|
289
|
+
],
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return payload;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function unavailable(
|
|
297
|
+
path: string,
|
|
298
|
+
absolutePath: string | null,
|
|
299
|
+
status: Exclude<SourceObservationStatus, "whole">,
|
|
300
|
+
source: SourceObservationSource,
|
|
301
|
+
reason?: string,
|
|
302
|
+
): SourceObservation {
|
|
303
|
+
return { path, absolutePath, status, source, reason };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function formatObservationPath(basePath: string, absolutePath: string, fallback: string): string {
|
|
307
|
+
const rel = relative(basePath, absolutePath);
|
|
308
|
+
if (rel && !rel.startsWith("..") && !isAbsolute(rel)) {
|
|
309
|
+
return rel.split(sep).join("/");
|
|
310
|
+
}
|
|
311
|
+
return fallback;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function isPathInsideRoot(rootPath: string, absolutePath: string): boolean {
|
|
315
|
+
const rel = relative(rootPath, absolutePath);
|
|
316
|
+
return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function containsGlobPattern(candidate: string): boolean {
|
|
320
|
+
return ["*", "?", "[", "]", "{", "}"].some((char) => candidate.includes(char));
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function readPathFromInput(input: { path?: unknown; file_path?: unknown }): string {
|
|
324
|
+
if (typeof input.path === "string") return input.path;
|
|
325
|
+
if (typeof input.file_path === "string") return input.file_path;
|
|
326
|
+
return "";
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function formatSize(bytes: number): string {
|
|
330
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
331
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
332
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function isBinaryOrImage(buffer: Buffer): boolean {
|
|
336
|
+
if (buffer.includes(0)) return true;
|
|
337
|
+
if (startsWith(buffer, [0xff, 0xd8, 0xff])) return true;
|
|
338
|
+
if (startsWith(buffer, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])) return true;
|
|
339
|
+
if (startsWithAscii(buffer, "GIF")) return true;
|
|
340
|
+
if (
|
|
341
|
+
startsWithAscii(buffer, "RIFF") &&
|
|
342
|
+
buffer.length >= 12 &&
|
|
343
|
+
buffer.subarray(8, 12).toString("ascii") === "WEBP"
|
|
344
|
+
) return true;
|
|
345
|
+
return false;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function startsWith(buffer: Buffer, signature: readonly number[]): boolean {
|
|
349
|
+
if (buffer.length < signature.length) return false;
|
|
350
|
+
return signature.every((byte, index) => buffer[index] === byte);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function startsWithAscii(buffer: Buffer, text: string): boolean {
|
|
354
|
+
return buffer.length >= text.length && buffer.subarray(0, text.length).toString("ascii") === text;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function countLines(text: string): number {
|
|
358
|
+
if (text.length === 0) return 0;
|
|
359
|
+
return text.endsWith("\n") ? text.split("\n").length - 1 : text.split("\n").length;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function fencedSource(text: string): string {
|
|
363
|
+
const longest = longestBacktickRun(text);
|
|
364
|
+
const fence = longest >= 3 ? "`".repeat(longest + 1) : "```";
|
|
365
|
+
return `${fence}\n${text}\n${fence}`;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function longestBacktickRun(text: string): number {
|
|
369
|
+
let longest = 0;
|
|
370
|
+
let current = 0;
|
|
371
|
+
for (const char of text) {
|
|
372
|
+
if (char === "`") {
|
|
373
|
+
current += 1;
|
|
374
|
+
longest = Math.max(longest, current);
|
|
375
|
+
} else {
|
|
376
|
+
current = 0;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return longest;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function firstText(content: unknown): string | null {
|
|
383
|
+
if (typeof content === "string") return content;
|
|
384
|
+
if (!Array.isArray(content)) return null;
|
|
385
|
+
const block = content.find((entry) =>
|
|
386
|
+
entry && typeof entry === "object" && "text" in entry,
|
|
387
|
+
) as { text?: unknown } | undefined;
|
|
388
|
+
return typeof block?.text === "string" ? block.text : null;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function isSourceContextMessage(message: unknown): boolean {
|
|
392
|
+
if (!message || typeof message !== "object") return false;
|
|
393
|
+
return firstText((message as { content?: unknown }).content)?.startsWith(SOURCE_CONTEXT_TITLE) === true;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function withoutExistingSourceContextMessages(messages: unknown[]): unknown[] {
|
|
397
|
+
return messages.filter((message) => !isSourceContextMessage(message));
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function withoutExistingSourceContextItems(items: unknown[]): unknown[] {
|
|
401
|
+
return items.filter((item) => !isSourceContextMessage(item));
|
|
402
|
+
}
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { existsSync, statSync } from "node:fs";
|
|
10
10
|
|
|
11
11
|
import {
|
|
12
|
+
getAllMilestones,
|
|
12
13
|
getMilestone,
|
|
13
14
|
getMilestoneSlices,
|
|
14
15
|
getSliceTasks,
|
|
@@ -41,17 +42,30 @@ function summaryMtimeIso(path: string): string | null {
|
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
export function detectMissingCompletionTimestampDrift(
|
|
44
|
-
|
|
45
|
+
_state: GSDState,
|
|
45
46
|
ctx: DriftContext,
|
|
46
47
|
): CompletionTimestampDrift[] {
|
|
47
48
|
if (!isDbAvailable()) return [];
|
|
48
|
-
const mid = state.activeMilestone?.id;
|
|
49
|
-
if (!mid) return [];
|
|
50
|
-
|
|
51
|
-
const milestone = getMilestone(mid);
|
|
52
|
-
if (!milestone) return [];
|
|
53
49
|
|
|
50
|
+
// Scan every milestone, not just the active one. Markdown artifacts exist on
|
|
51
|
+
// disk for all milestones, so a user can manually complete a queued/parked
|
|
52
|
+
// milestone (edit the roadmap + drop a SUMMARY) and leave completed_at=null
|
|
53
|
+
// in the DB. Gating on the active milestone left that drift unrepaired until
|
|
54
|
+
// the milestone happened to become active.
|
|
54
55
|
const drifts: CompletionTimestampDrift[] = [];
|
|
56
|
+
for (const { id: mid } of getAllMilestones()) {
|
|
57
|
+
collectMilestoneCompletionDrift(mid, ctx, drifts);
|
|
58
|
+
}
|
|
59
|
+
return drifts;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function collectMilestoneCompletionDrift(
|
|
63
|
+
mid: string,
|
|
64
|
+
ctx: DriftContext,
|
|
65
|
+
drifts: CompletionTimestampDrift[],
|
|
66
|
+
): void {
|
|
67
|
+
const milestone = getMilestone(mid);
|
|
68
|
+
if (!milestone) return;
|
|
55
69
|
|
|
56
70
|
// Milestone-level
|
|
57
71
|
if (
|
|
@@ -105,8 +119,6 @@ export function detectMissingCompletionTimestampDrift(
|
|
|
105
119
|
}
|
|
106
120
|
}
|
|
107
121
|
}
|
|
108
|
-
|
|
109
|
-
return drifts;
|
|
110
122
|
}
|
|
111
123
|
|
|
112
124
|
export function repairMissingCompletionTimestamp(
|
|
@@ -61,6 +61,18 @@ function isRepairableStaleRenderReason(reason: string): boolean {
|
|
|
61
61
|
);
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
function canonicalizeMilestoneId(dirSegment: string): string {
|
|
65
|
+
if (getMilestone(dirSegment)) return dirSegment;
|
|
66
|
+
const suffixId = dirSegment.match(/^(M\d+-[a-z0-9]{6})(?:$|-)/)?.[1];
|
|
67
|
+
if (suffixId && getMilestone(suffixId)) return suffixId;
|
|
68
|
+
|
|
69
|
+
// Descriptor layout: e.g. M001-DESCRIPTOR → M001
|
|
70
|
+
const baseId = dirSegment.match(/^(M\d+)(?:$|-)/i)?.[1];
|
|
71
|
+
if (baseId && getMilestone(baseId)) return baseId;
|
|
72
|
+
|
|
73
|
+
return suffixId ?? baseId ?? dirSegment;
|
|
74
|
+
}
|
|
75
|
+
|
|
64
76
|
function resolveRoadmapMilestoneIdFromPath(normPath: string): string {
|
|
65
77
|
const milestoneMatch = normPath.match(/milestones\/([^/]+)\//);
|
|
66
78
|
if (!milestoneMatch) {
|
|
@@ -106,7 +118,19 @@ async function repairStaleRenderFromBasePath(
|
|
|
106
118
|
`stale-render drift: plan path missing milestone/slice segments: ${record.renderPath}`,
|
|
107
119
|
);
|
|
108
120
|
}
|
|
109
|
-
|
|
121
|
+
const milestoneId = canonicalizeMilestoneId(pathMatch[1]);
|
|
122
|
+
const wrote = await renderPlanCheckboxes(
|
|
123
|
+
basePath,
|
|
124
|
+
milestoneId,
|
|
125
|
+
pathMatch[2],
|
|
126
|
+
record.renderPath,
|
|
127
|
+
);
|
|
128
|
+
if (!wrote) {
|
|
129
|
+
throw new Error(
|
|
130
|
+
`stale-render drift: plan re-render wrote nothing for ${milestoneId}/${pathMatch[2]} ` +
|
|
131
|
+
`(${record.renderPath}); slice has no tasks or its path is unresolvable`,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
110
134
|
return;
|
|
111
135
|
}
|
|
112
136
|
|
|
@@ -120,7 +144,15 @@ async function repairStaleRenderFromBasePath(
|
|
|
120
144
|
`stale-render drift: task summary path/reason malformed: ${record.renderPath} reason=${reason}`,
|
|
121
145
|
);
|
|
122
146
|
}
|
|
123
|
-
|
|
147
|
+
const milestoneId = canonicalizeMilestoneId(pathMatch[1]);
|
|
148
|
+
const wrote = await renderTaskSummary(basePath, milestoneId, pathMatch[2], taskMatch[1]);
|
|
149
|
+
if (!wrote) {
|
|
150
|
+
throw new Error(
|
|
151
|
+
`stale-render drift: task summary re-render wrote nothing for ` +
|
|
152
|
+
`${milestoneId}/${pathMatch[2]}/${taskMatch[1]} (${record.renderPath}); ` +
|
|
153
|
+
`task has no summary in DB or its slice path is unresolvable`,
|
|
154
|
+
);
|
|
155
|
+
}
|
|
124
156
|
return;
|
|
125
157
|
}
|
|
126
158
|
|
|
@@ -131,7 +163,7 @@ async function repairStaleRenderFromBasePath(
|
|
|
131
163
|
`stale-render drift: slice summary path missing milestone/slice segments: ${record.renderPath}`,
|
|
132
164
|
);
|
|
133
165
|
}
|
|
134
|
-
const milestoneId = pathMatch[1];
|
|
166
|
+
const milestoneId = canonicalizeMilestoneId(pathMatch[1]);
|
|
135
167
|
const sliceId = pathMatch[2];
|
|
136
168
|
const slice = getSlice(milestoneId, sliceId);
|
|
137
169
|
const uatPath = join(dirname(record.renderPath), buildSliceFileName(sliceId, "UAT"));
|
|
@@ -139,7 +171,14 @@ async function repairStaleRenderFromBasePath(
|
|
|
139
171
|
if (slice?.full_uat_md && !existsSync(uatPath)) {
|
|
140
172
|
setSliceSummaryMd(milestoneId, sliceId, slice.full_summary_md ?? "", "");
|
|
141
173
|
}
|
|
142
|
-
await renderSliceSummary(basePath, milestoneId, sliceId);
|
|
174
|
+
const wrote = await renderSliceSummary(basePath, milestoneId, sliceId);
|
|
175
|
+
if (!wrote) {
|
|
176
|
+
throw new Error(
|
|
177
|
+
`stale-render drift: slice summary re-render wrote nothing for ` +
|
|
178
|
+
`${milestoneId}/${sliceId} (${record.renderPath}); slice has no summary/UAT ` +
|
|
179
|
+
`in DB or its path is unresolvable`,
|
|
180
|
+
);
|
|
181
|
+
}
|
|
143
182
|
return;
|
|
144
183
|
}
|
|
145
184
|
|
|
@@ -152,7 +191,7 @@ async function repairStaleRenderFromBasePath(
|
|
|
152
191
|
}
|
|
153
192
|
// When UAT.md is removed from disk, mirror that intent by clearing stale
|
|
154
193
|
// persisted UAT content instead of rehydrating it back onto disk.
|
|
155
|
-
const milestoneId = pathMatch[1];
|
|
194
|
+
const milestoneId = canonicalizeMilestoneId(pathMatch[1]);
|
|
156
195
|
const sliceId = pathMatch[2];
|
|
157
196
|
const slice = getSlice(milestoneId, sliceId);
|
|
158
197
|
if (!slice) {
|
|
@@ -10,6 +10,11 @@ import {
|
|
|
10
10
|
readSessionLockData,
|
|
11
11
|
removeStaleSessionLock,
|
|
12
12
|
} from "../../session-lock.js";
|
|
13
|
+
import { clearStaleWorkerLock } from "../../crash-recovery.js";
|
|
14
|
+
import { findStaleWorkerForProject } from "../../db/auto-workers.js";
|
|
15
|
+
import { isDbAvailable } from "../../gsd-db.js";
|
|
16
|
+
import { normalizeRealPath } from "../../paths.js";
|
|
17
|
+
import { logWarning } from "../../workflow-logger.js";
|
|
13
18
|
import type { GSDState } from "../../types.js";
|
|
14
19
|
import type { DriftContext, DriftHandler, DriftRecord } from "../types.js";
|
|
15
20
|
|
|
@@ -20,23 +25,46 @@ export function detectStaleWorkerDrift(
|
|
|
20
25
|
ctx: DriftContext,
|
|
21
26
|
): StaleWorkerDrift[] {
|
|
22
27
|
const data = readSessionLockData(ctx.basePath);
|
|
23
|
-
if (
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
28
|
+
if (data && typeof data.pid === "number") {
|
|
29
|
+
return isSessionLockProcessAlive(data)
|
|
30
|
+
? []
|
|
31
|
+
: [{ kind: "stale-worker", lockPath: effectiveLockFile(), pid: data.pid }];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// The lock file is missing or unparseable. It is not the only source of
|
|
35
|
+
// truth: a crashed worker can leave a workers row 'active' with held leases
|
|
36
|
+
// and in-flight dispatches even when its lock file is gone. Fall back to the
|
|
37
|
+
// DB worker registry so that state is still detected and repaired.
|
|
38
|
+
if (isDbAvailable()) {
|
|
39
|
+
try {
|
|
40
|
+
const stale = findStaleWorkerForProject(normalizeRealPath(ctx.basePath));
|
|
41
|
+
if (stale && typeof stale.pid === "number") {
|
|
42
|
+
return [{ kind: "stale-worker", lockPath: effectiveLockFile(), pid: stale.pid }];
|
|
43
|
+
}
|
|
44
|
+
} catch (err) {
|
|
45
|
+
// Best-effort: detection must never throw and abort the reconcile cycle.
|
|
46
|
+
logWarning(
|
|
47
|
+
"reconcile",
|
|
48
|
+
`stale-worker DB fallback detection failed: ${(err as Error).message}`,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return [];
|
|
34
54
|
}
|
|
35
55
|
|
|
36
56
|
export function repairStaleWorker(_record: StaleWorkerDrift, ctx: DriftContext): void {
|
|
37
57
|
// removeStaleSessionLock is idempotent: it re-reads lock state and is a
|
|
38
58
|
// no-op when the lock is held by an alive process. Safe under cap=2 retry.
|
|
39
59
|
removeStaleSessionLock(ctx.basePath);
|
|
60
|
+
|
|
61
|
+
// Removing the lock file alone leaves the DB-side worker state dangling: the
|
|
62
|
+
// dead worker's milestone_leases stay 'held' and its unit_dispatches stay
|
|
63
|
+
// 'running'/'claimed', blocking new claims until the lease TTL expires.
|
|
64
|
+
// clearStaleWorkerLock cancels those dispatches, releases the leases, and
|
|
65
|
+
// marks the worker stopping — the same cleanup the startup crash-recovery
|
|
66
|
+
// path performs. It is DB-gated, idempotent, and best-effort.
|
|
67
|
+
clearStaleWorkerLock(ctx.basePath);
|
|
40
68
|
}
|
|
41
69
|
|
|
42
70
|
export const staleWorkerHandler: DriftHandler<StaleWorkerDrift> = {
|