gsd-pi 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +341 -0
- package/dist/app-paths.d.ts +4 -0
- package/dist/app-paths.js +6 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +35 -0
- package/dist/loader.d.ts +2 -0
- package/dist/loader.js +69 -0
- package/dist/modes/interactive/theme/dark.json +85 -0
- package/dist/modes/interactive/theme/light.json +84 -0
- package/dist/modes/interactive/theme/theme-schema.json +335 -0
- package/dist/modes/interactive/theme/theme.d.ts +78 -0
- package/dist/modes/interactive/theme/theme.d.ts.map +1 -0
- package/dist/modes/interactive/theme/theme.js +949 -0
- package/dist/modes/interactive/theme/theme.js.map +1 -0
- package/dist/resource-loader.d.ts +22 -0
- package/dist/resource-loader.js +48 -0
- package/dist/wizard.d.ts +20 -0
- package/dist/wizard.js +132 -0
- package/package.json +39 -0
- package/pkg/dist/modes/interactive/theme/dark.json +85 -0
- package/pkg/dist/modes/interactive/theme/light.json +84 -0
- package/pkg/dist/modes/interactive/theme/theme-schema.json +335 -0
- package/pkg/dist/modes/interactive/theme/theme.d.ts +78 -0
- package/pkg/dist/modes/interactive/theme/theme.d.ts.map +1 -0
- package/pkg/dist/modes/interactive/theme/theme.js +949 -0
- package/pkg/dist/modes/interactive/theme/theme.js.map +1 -0
- package/pkg/package.json +8 -0
- package/scripts/postinstall.js +10 -0
- package/src/resources/AGENTS.md +204 -0
- package/src/resources/GSD-WORKFLOW.md +661 -0
- package/src/resources/agents/researcher.md +29 -0
- package/src/resources/agents/scout.md +56 -0
- package/src/resources/agents/worker.md +31 -0
- package/src/resources/extensions/ask-user-questions.ts +200 -0
- package/src/resources/extensions/bg-shell/index.ts +2554 -0
- package/src/resources/extensions/browser-tools/BROWSER-TOOLS-V2-PROPOSAL.md +1277 -0
- package/src/resources/extensions/browser-tools/core.js +1057 -0
- package/src/resources/extensions/browser-tools/index.ts +4916 -0
- package/src/resources/extensions/browser-tools/package.json +20 -0
- package/src/resources/extensions/context7/index.ts +428 -0
- package/src/resources/extensions/context7/package.json +11 -0
- package/src/resources/extensions/get-secrets-from-user.ts +352 -0
- package/src/resources/extensions/gsd/activity-log.ts +48 -0
- package/src/resources/extensions/gsd/auto.ts +2032 -0
- package/src/resources/extensions/gsd/commands.ts +292 -0
- package/src/resources/extensions/gsd/crash-recovery.ts +85 -0
- package/src/resources/extensions/gsd/dashboard-overlay.ts +516 -0
- package/src/resources/extensions/gsd/docs/preferences-reference.md +103 -0
- package/src/resources/extensions/gsd/doctor.ts +683 -0
- package/src/resources/extensions/gsd/files.ts +730 -0
- package/src/resources/extensions/gsd/gitignore.ts +104 -0
- package/src/resources/extensions/gsd/guided-flow.ts +800 -0
- package/src/resources/extensions/gsd/index.ts +418 -0
- package/src/resources/extensions/gsd/metrics.ts +372 -0
- package/src/resources/extensions/gsd/observability-validator.ts +408 -0
- package/src/resources/extensions/gsd/package.json +11 -0
- package/src/resources/extensions/gsd/paths.ts +308 -0
- package/src/resources/extensions/gsd/preferences.ts +600 -0
- package/src/resources/extensions/gsd/prompt-loader.ts +50 -0
- package/src/resources/extensions/gsd/prompts/complete-milestone.md +25 -0
- package/src/resources/extensions/gsd/prompts/complete-slice.md +27 -0
- package/src/resources/extensions/gsd/prompts/discuss.md +151 -0
- package/src/resources/extensions/gsd/prompts/doctor-heal.md +29 -0
- package/src/resources/extensions/gsd/prompts/execute-task.md +64 -0
- package/src/resources/extensions/gsd/prompts/guided-complete-slice.md +1 -0
- package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +3 -0
- package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +59 -0
- package/src/resources/extensions/gsd/prompts/guided-execute-task.md +1 -0
- package/src/resources/extensions/gsd/prompts/guided-plan-milestone.md +23 -0
- package/src/resources/extensions/gsd/prompts/guided-plan-slice.md +1 -0
- package/src/resources/extensions/gsd/prompts/guided-research-slice.md +11 -0
- package/src/resources/extensions/gsd/prompts/guided-resume-task.md +1 -0
- package/src/resources/extensions/gsd/prompts/plan-milestone.md +47 -0
- package/src/resources/extensions/gsd/prompts/plan-slice.md +63 -0
- package/src/resources/extensions/gsd/prompts/queue.md +85 -0
- package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +48 -0
- package/src/resources/extensions/gsd/prompts/replan-slice.md +39 -0
- package/src/resources/extensions/gsd/prompts/research-milestone.md +37 -0
- package/src/resources/extensions/gsd/prompts/research-slice.md +28 -0
- package/src/resources/extensions/gsd/prompts/run-uat.md +109 -0
- package/src/resources/extensions/gsd/prompts/system.md +220 -0
- package/src/resources/extensions/gsd/session-forensics.ts +487 -0
- package/src/resources/extensions/gsd/skill-discovery.ts +137 -0
- package/src/resources/extensions/gsd/state.ts +439 -0
- package/src/resources/extensions/gsd/templates/context.md +76 -0
- package/src/resources/extensions/gsd/templates/decisions.md +8 -0
- package/src/resources/extensions/gsd/templates/milestone-summary.md +73 -0
- package/src/resources/extensions/gsd/templates/plan.md +133 -0
- package/src/resources/extensions/gsd/templates/preferences.md +15 -0
- package/src/resources/extensions/gsd/templates/project.md +31 -0
- package/src/resources/extensions/gsd/templates/reassessment.md +28 -0
- package/src/resources/extensions/gsd/templates/requirements.md +81 -0
- package/src/resources/extensions/gsd/templates/research.md +46 -0
- package/src/resources/extensions/gsd/templates/roadmap.md +118 -0
- package/src/resources/extensions/gsd/templates/slice-context.md +58 -0
- package/src/resources/extensions/gsd/templates/slice-summary.md +99 -0
- package/src/resources/extensions/gsd/templates/state.md +19 -0
- package/src/resources/extensions/gsd/templates/task-plan.md +52 -0
- package/src/resources/extensions/gsd/templates/task-summary.md +57 -0
- package/src/resources/extensions/gsd/templates/uat.md +54 -0
- package/src/resources/extensions/gsd/types.ts +159 -0
- package/src/resources/extensions/gsd/unit-runtime.ts +162 -0
- package/src/resources/extensions/gsd/workspace-index.ts +203 -0
- package/src/resources/extensions/gsd/worktree.ts +182 -0
- package/src/resources/extensions/plan-mode/README.md +65 -0
- package/src/resources/extensions/plan-mode/index.ts +521 -0
- package/src/resources/extensions/plan-mode/utils.ts +168 -0
- package/src/resources/extensions/search-the-web/cache.ts +70 -0
- package/src/resources/extensions/search-the-web/format.ts +134 -0
- package/src/resources/extensions/search-the-web/http.ts +147 -0
- package/src/resources/extensions/search-the-web/index.ts +46 -0
- package/src/resources/extensions/search-the-web/tool-fetch-page.ts +374 -0
- package/src/resources/extensions/search-the-web/tool-search.ts +424 -0
- package/src/resources/extensions/search-the-web/url-utils.ts +91 -0
- package/src/resources/extensions/shared/interview-ui.ts +613 -0
- package/src/resources/extensions/shared/next-action-ui.ts +197 -0
- package/src/resources/extensions/shared/progress-widget.ts +282 -0
- package/src/resources/extensions/shared/thinking-widget.ts +107 -0
- package/src/resources/extensions/shared/ui.ts +400 -0
- package/src/resources/extensions/shared/wizard-ui.ts +551 -0
- package/src/resources/extensions/slash-commands/audit.ts +88 -0
- package/src/resources/extensions/slash-commands/create-extension.ts +297 -0
- package/src/resources/extensions/slash-commands/create-slash-command.ts +234 -0
- package/src/resources/extensions/slash-commands/gsd-run.ts +34 -0
- package/src/resources/extensions/slash-commands/index.ts +12 -0
- package/src/resources/extensions/subagent/agents.ts +126 -0
- package/src/resources/extensions/subagent/index.ts +1021 -0
- package/src/resources/extensions/worktree/index.ts +420 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: {{taskId}}
|
|
3
|
+
parent: {{sliceId}}
|
|
4
|
+
milestone: {{milestoneId}}
|
|
5
|
+
provides:
|
|
6
|
+
- {{whatThisTaskProvides}}
|
|
7
|
+
key_files:
|
|
8
|
+
- {{filePath}}
|
|
9
|
+
key_decisions:
|
|
10
|
+
- {{decision}}
|
|
11
|
+
patterns_established:
|
|
12
|
+
- {{pattern}}
|
|
13
|
+
observability_surfaces:
|
|
14
|
+
- {{status endpoint, structured log, persisted failure state, diagnostic command, or none}}
|
|
15
|
+
duration: {{duration}}
|
|
16
|
+
verification_result: passed
|
|
17
|
+
completed_at: {{date}}
|
|
18
|
+
# Set blocker_discovered: true only if execution revealed the remaining slice plan
|
|
19
|
+
# is fundamentally invalid (wrong API, missing capability, architectural mismatch).
|
|
20
|
+
# Do NOT set true for ordinary bugs, minor deviations, or fixable issues.
|
|
21
|
+
blocker_discovered: false
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
# {{taskId}}: {{taskTitle}}
|
|
25
|
+
|
|
26
|
+
<!-- One-liner must say what actually shipped, not just that work completed.
|
|
27
|
+
Good: "Added retry-aware worker status logging"
|
|
28
|
+
Bad: "Implemented logging improvements" -->
|
|
29
|
+
|
|
30
|
+
**{{oneLiner}}**
|
|
31
|
+
|
|
32
|
+
## What Happened
|
|
33
|
+
|
|
34
|
+
{{narrative}}
|
|
35
|
+
|
|
36
|
+
## Verification
|
|
37
|
+
|
|
38
|
+
{{whatWasVerifiedAndHow — commands run, tests passed, behavior confirmed}}
|
|
39
|
+
|
|
40
|
+
## Diagnostics
|
|
41
|
+
|
|
42
|
+
{{howToInspectWhatThisTaskBuiltLater — status surfaces, logs, error shapes, failure artifacts, or none}}
|
|
43
|
+
|
|
44
|
+
## Deviations
|
|
45
|
+
|
|
46
|
+
<!-- Deviations are unplanned changes to the written task plan, not ordinary debugging during implementation. -->
|
|
47
|
+
|
|
48
|
+
{{deviationsFromPlan_OR_none}}
|
|
49
|
+
|
|
50
|
+
## Known Issues
|
|
51
|
+
|
|
52
|
+
{{issuesDiscoveredButNotFixed_OR_none}}
|
|
53
|
+
|
|
54
|
+
## Files Created/Modified
|
|
55
|
+
|
|
56
|
+
- `{{filePath}}` — {{description}}
|
|
57
|
+
- `{{filePath}}` — {{description}}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# {{sliceId}}: {{sliceTitle}} — UAT
|
|
2
|
+
|
|
3
|
+
**Milestone:** {{milestoneId}}
|
|
4
|
+
**Written:** {{date}}
|
|
5
|
+
|
|
6
|
+
## UAT Type
|
|
7
|
+
|
|
8
|
+
- UAT mode: {{artifact-driven | live-runtime | human-experience | mixed}}
|
|
9
|
+
- Why this mode is sufficient: {{reason}}
|
|
10
|
+
|
|
11
|
+
## Preconditions
|
|
12
|
+
|
|
13
|
+
{{whatMustBeTrueBeforeTesting — server running, data seeded, etc.}}
|
|
14
|
+
|
|
15
|
+
## Smoke Test
|
|
16
|
+
|
|
17
|
+
{{oneQuickCheckThatConfirmsTheSliceBasicallyWorks}}
|
|
18
|
+
|
|
19
|
+
## Test Cases
|
|
20
|
+
|
|
21
|
+
### 1. {{testName}}
|
|
22
|
+
|
|
23
|
+
1. {{step}}
|
|
24
|
+
2. {{step}}
|
|
25
|
+
3. **Expected:** {{expected}}
|
|
26
|
+
|
|
27
|
+
### 2. {{testName}}
|
|
28
|
+
|
|
29
|
+
1. {{step}}
|
|
30
|
+
2. **Expected:** {{expected}}
|
|
31
|
+
|
|
32
|
+
## Edge Cases
|
|
33
|
+
|
|
34
|
+
### {{edgeCaseName}}
|
|
35
|
+
|
|
36
|
+
1. {{step}}
|
|
37
|
+
2. **Expected:** {{expected}}
|
|
38
|
+
|
|
39
|
+
## Failure Signals
|
|
40
|
+
|
|
41
|
+
- {{whatWouldIndicateSomethingIsBroken — errors, missing UI, wrong data}}
|
|
42
|
+
|
|
43
|
+
## Requirements Proved By This UAT
|
|
44
|
+
|
|
45
|
+
- {{requirementIdOr_none}} — {{what this UAT proves}}
|
|
46
|
+
|
|
47
|
+
## Not Proven By This UAT
|
|
48
|
+
|
|
49
|
+
- {{what this UAT intentionally does not prove}}
|
|
50
|
+
- {{remaining live/runtime/operational gaps, if any}}
|
|
51
|
+
|
|
52
|
+
## Notes for Tester
|
|
53
|
+
|
|
54
|
+
{{anythingTheHumanShouldKnow — known rough edges, things to ignore, areas needing gut check}}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// GSD Extension — Core Type Definitions
|
|
2
|
+
// Types consumed by state derivation, file parsing, and status display.
|
|
3
|
+
// Pure interfaces — no logic, no runtime dependencies.
|
|
4
|
+
|
|
5
|
+
// ─── Enums & Literal Unions ────────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
export type RiskLevel = 'low' | 'medium' | 'high';
|
|
8
|
+
export type Phase = 'pre-planning' | 'discussing' | 'researching' | 'planning' | 'executing' | 'verifying' | 'summarizing' | 'advancing' | 'completing-milestone' | 'replanning-slice' | 'complete' | 'paused' | 'blocked';
|
|
9
|
+
export type ContinueStatus = 'in_progress' | 'interrupted' | 'compacted';
|
|
10
|
+
|
|
11
|
+
// ─── Roadmap (Milestone-level) ─────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
export interface RoadmapSliceEntry {
|
|
14
|
+
id: string; // e.g. "S01"
|
|
15
|
+
title: string; // e.g. "Types + File I/O + Git Operations"
|
|
16
|
+
risk: RiskLevel;
|
|
17
|
+
depends: string[]; // e.g. ["S01", "S02"]
|
|
18
|
+
done: boolean;
|
|
19
|
+
demo: string; // the "After this:" sentence
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface BoundaryMapEntry {
|
|
23
|
+
fromSlice: string; // e.g. "S01"
|
|
24
|
+
toSlice: string; // e.g. "S02" or "terminal"
|
|
25
|
+
produces: string; // raw text block of what this slice produces
|
|
26
|
+
consumes: string; // raw text block of what it consumes (or "nothing")
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface Roadmap {
|
|
30
|
+
title: string; // e.g. "M001: GSD Extension — Hierarchical Planning with Auto Mode"
|
|
31
|
+
vision: string;
|
|
32
|
+
successCriteria: string[];
|
|
33
|
+
slices: RoadmapSliceEntry[];
|
|
34
|
+
boundaryMap: BoundaryMapEntry[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ─── Slice Plan ────────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
export interface TaskPlanEntry {
|
|
40
|
+
id: string; // e.g. "T01"
|
|
41
|
+
title: string; // e.g. "Core Type Definitions"
|
|
42
|
+
description: string;
|
|
43
|
+
done: boolean;
|
|
44
|
+
estimate: string; // e.g. "30m", "2h" — informational only
|
|
45
|
+
files?: string[]; // e.g. ["types.ts", "files.ts"] — extracted from "- Files:" subline
|
|
46
|
+
verify?: string; // e.g. "run tests" — extracted from "- Verify:" subline
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface SlicePlan {
|
|
50
|
+
id: string; // e.g. "S01"
|
|
51
|
+
title: string; // from the H1
|
|
52
|
+
goal: string;
|
|
53
|
+
demo: string;
|
|
54
|
+
mustHaves: string[]; // top-level must-have bullet points
|
|
55
|
+
tasks: TaskPlanEntry[];
|
|
56
|
+
filesLikelyTouched: string[];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ─── Summary (Task & Slice level) ──────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
export interface SummaryRequires {
|
|
62
|
+
slice: string;
|
|
63
|
+
provides: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface SummaryFrontmatter {
|
|
67
|
+
id: string;
|
|
68
|
+
parent: string;
|
|
69
|
+
milestone: string;
|
|
70
|
+
provides: string[];
|
|
71
|
+
requires: SummaryRequires[];
|
|
72
|
+
affects: string[];
|
|
73
|
+
key_files: string[];
|
|
74
|
+
key_decisions: string[];
|
|
75
|
+
patterns_established: string[];
|
|
76
|
+
drill_down_paths: string[];
|
|
77
|
+
observability_surfaces: string[];
|
|
78
|
+
duration: string;
|
|
79
|
+
verification_result: string;
|
|
80
|
+
completed_at: string;
|
|
81
|
+
blocker_discovered: boolean;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface FileModified {
|
|
85
|
+
path: string;
|
|
86
|
+
description: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface Summary {
|
|
90
|
+
frontmatter: SummaryFrontmatter;
|
|
91
|
+
title: string;
|
|
92
|
+
oneLiner: string;
|
|
93
|
+
whatHappened: string;
|
|
94
|
+
deviations: string;
|
|
95
|
+
filesModified: FileModified[];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ─── Continue-Here ─────────────────────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
export interface ContinueFrontmatter {
|
|
101
|
+
milestone: string;
|
|
102
|
+
slice: string;
|
|
103
|
+
task: string;
|
|
104
|
+
step: number;
|
|
105
|
+
totalSteps: number;
|
|
106
|
+
status: ContinueStatus;
|
|
107
|
+
savedAt: string;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface Continue {
|
|
111
|
+
frontmatter: ContinueFrontmatter;
|
|
112
|
+
completedWork: string;
|
|
113
|
+
remainingWork: string;
|
|
114
|
+
decisions: string;
|
|
115
|
+
context: string;
|
|
116
|
+
nextAction: string;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ─── GSD State (Derived Dashboard) ────────────────────────────────────────
|
|
120
|
+
|
|
121
|
+
export interface ActiveRef {
|
|
122
|
+
id: string;
|
|
123
|
+
title: string;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface MilestoneRegistryEntry {
|
|
127
|
+
id: string;
|
|
128
|
+
title: string;
|
|
129
|
+
status: 'complete' | 'active' | 'pending';
|
|
130
|
+
/** Milestone IDs that must be complete before this milestone becomes active. Populated from CONTEXT.md YAML frontmatter. */
|
|
131
|
+
dependsOn?: string[];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export interface RequirementCounts {
|
|
135
|
+
active: number;
|
|
136
|
+
validated: number;
|
|
137
|
+
deferred: number;
|
|
138
|
+
outOfScope: number;
|
|
139
|
+
blocked: number;
|
|
140
|
+
total: number;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export interface GSDState {
|
|
144
|
+
activeMilestone: ActiveRef | null;
|
|
145
|
+
activeSlice: ActiveRef | null;
|
|
146
|
+
activeTask: ActiveRef | null;
|
|
147
|
+
phase: Phase;
|
|
148
|
+
recentDecisions: string[];
|
|
149
|
+
blockers: string[];
|
|
150
|
+
nextAction: string;
|
|
151
|
+
activeBranch?: string;
|
|
152
|
+
registry: MilestoneRegistryEntry[];
|
|
153
|
+
requirements?: RequirementCounts;
|
|
154
|
+
progress?: {
|
|
155
|
+
milestones: { done: number; total: number };
|
|
156
|
+
slices?: { done: number; total: number };
|
|
157
|
+
tasks?: { done: number; total: number };
|
|
158
|
+
};
|
|
159
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
gsdRoot,
|
|
5
|
+
relSliceFile,
|
|
6
|
+
relTaskFile,
|
|
7
|
+
resolveSliceFile,
|
|
8
|
+
resolveTaskFile,
|
|
9
|
+
} from "./paths.ts";
|
|
10
|
+
import { loadFile, parseTaskPlanMustHaves, countMustHavesMentionedInSummary } from "./files.ts";
|
|
11
|
+
|
|
12
|
+
export type UnitRuntimePhase =
|
|
13
|
+
| "dispatched"
|
|
14
|
+
| "wrapup-warning-sent"
|
|
15
|
+
| "timeout"
|
|
16
|
+
| "recovered"
|
|
17
|
+
| "finalized"
|
|
18
|
+
| "paused";
|
|
19
|
+
|
|
20
|
+
export interface ExecuteTaskRecoveryStatus {
|
|
21
|
+
planPath: string;
|
|
22
|
+
summaryPath: string;
|
|
23
|
+
summaryExists: boolean;
|
|
24
|
+
taskChecked: boolean;
|
|
25
|
+
nextActionAdvanced: boolean;
|
|
26
|
+
mustHaveCount: number;
|
|
27
|
+
mustHavesMentionedInSummary: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface AutoUnitRuntimeRecord {
|
|
31
|
+
version: 1;
|
|
32
|
+
unitType: string;
|
|
33
|
+
unitId: string;
|
|
34
|
+
startedAt: number;
|
|
35
|
+
updatedAt: number;
|
|
36
|
+
phase: UnitRuntimePhase;
|
|
37
|
+
wrapupWarningSent: boolean;
|
|
38
|
+
timeoutAt: number | null;
|
|
39
|
+
lastProgressAt: number;
|
|
40
|
+
progressCount: number;
|
|
41
|
+
lastProgressKind: string;
|
|
42
|
+
recovery?: ExecuteTaskRecoveryStatus;
|
|
43
|
+
recoveryAttempts?: number;
|
|
44
|
+
lastRecoveryReason?: "idle" | "hard";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function runtimeDir(basePath: string): string {
|
|
48
|
+
return join(gsdRoot(basePath), "runtime", "units");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function runtimePath(basePath: string, unitType: string, unitId: string): string {
|
|
52
|
+
return join(runtimeDir(basePath), `${unitType}-${unitId.replace(/[\/]/g, "-")}.json`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function writeUnitRuntimeRecord(
|
|
56
|
+
basePath: string,
|
|
57
|
+
unitType: string,
|
|
58
|
+
unitId: string,
|
|
59
|
+
startedAt: number,
|
|
60
|
+
updates: Partial<AutoUnitRuntimeRecord> = {},
|
|
61
|
+
): AutoUnitRuntimeRecord {
|
|
62
|
+
const dir = runtimeDir(basePath);
|
|
63
|
+
mkdirSync(dir, { recursive: true });
|
|
64
|
+
const path = runtimePath(basePath, unitType, unitId);
|
|
65
|
+
const prev = readUnitRuntimeRecord(basePath, unitType, unitId);
|
|
66
|
+
const next: AutoUnitRuntimeRecord = {
|
|
67
|
+
version: 1,
|
|
68
|
+
unitType,
|
|
69
|
+
unitId,
|
|
70
|
+
startedAt,
|
|
71
|
+
updatedAt: Date.now(),
|
|
72
|
+
phase: updates.phase ?? prev?.phase ?? "dispatched",
|
|
73
|
+
wrapupWarningSent: updates.wrapupWarningSent ?? prev?.wrapupWarningSent ?? false,
|
|
74
|
+
timeoutAt: updates.timeoutAt ?? prev?.timeoutAt ?? null,
|
|
75
|
+
lastProgressAt: updates.lastProgressAt ?? prev?.lastProgressAt ?? Date.now(),
|
|
76
|
+
progressCount: updates.progressCount ?? prev?.progressCount ?? 0,
|
|
77
|
+
lastProgressKind: updates.lastProgressKind ?? prev?.lastProgressKind ?? "dispatch",
|
|
78
|
+
recovery: updates.recovery ?? prev?.recovery,
|
|
79
|
+
recoveryAttempts: updates.recoveryAttempts ?? prev?.recoveryAttempts ?? 0,
|
|
80
|
+
lastRecoveryReason: updates.lastRecoveryReason ?? prev?.lastRecoveryReason,
|
|
81
|
+
};
|
|
82
|
+
writeFileSync(path, JSON.stringify(next, null, 2) + "\n", "utf-8");
|
|
83
|
+
return next;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function readUnitRuntimeRecord(basePath: string, unitType: string, unitId: string): AutoUnitRuntimeRecord | null {
|
|
87
|
+
const path = runtimePath(basePath, unitType, unitId);
|
|
88
|
+
if (!existsSync(path)) return null;
|
|
89
|
+
try {
|
|
90
|
+
return JSON.parse(readFileSync(path, "utf-8")) as AutoUnitRuntimeRecord;
|
|
91
|
+
} catch {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function clearUnitRuntimeRecord(basePath: string, unitType: string, unitId: string): void {
|
|
97
|
+
const path = runtimePath(basePath, unitType, unitId);
|
|
98
|
+
if (existsSync(path)) unlinkSync(path);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function inspectExecuteTaskDurability(
|
|
102
|
+
basePath: string,
|
|
103
|
+
unitId: string,
|
|
104
|
+
): Promise<ExecuteTaskRecoveryStatus | null> {
|
|
105
|
+
const [mid, sid, tid] = unitId.split("/");
|
|
106
|
+
if (!mid || !sid || !tid) return null;
|
|
107
|
+
|
|
108
|
+
const planAbs = resolveSliceFile(basePath, mid, sid, "PLAN");
|
|
109
|
+
const summaryAbs = resolveTaskFile(basePath, mid, sid, tid, "SUMMARY");
|
|
110
|
+
const stateAbs = join(gsdRoot(basePath), "STATE.md");
|
|
111
|
+
|
|
112
|
+
const planPath = relSliceFile(basePath, mid, sid, "PLAN");
|
|
113
|
+
const summaryPath = relTaskFile(basePath, mid, sid, tid, "SUMMARY");
|
|
114
|
+
|
|
115
|
+
const planContent = planAbs ? await loadFile(planAbs) : null;
|
|
116
|
+
const stateContent = existsSync(stateAbs) ? readFileSync(stateAbs, "utf-8") : "";
|
|
117
|
+
const summaryExists = !!(summaryAbs && existsSync(summaryAbs));
|
|
118
|
+
|
|
119
|
+
const escapedTid = tid.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
120
|
+
const taskChecked = !!planContent && new RegExp(`^- \\[[xX]\\] \\*\\*${escapedTid}:`, "m").test(planContent);
|
|
121
|
+
const nextActionAdvanced = !new RegExp(`Execute ${tid}\\b`).test(stateContent);
|
|
122
|
+
|
|
123
|
+
// Must-have coverage: load task plan and count mentions in summary
|
|
124
|
+
let mustHaveCount = 0;
|
|
125
|
+
let mustHavesMentionedInSummary = 0;
|
|
126
|
+
|
|
127
|
+
const taskPlanAbs = resolveTaskFile(basePath, mid, sid, tid, "PLAN");
|
|
128
|
+
if (taskPlanAbs) {
|
|
129
|
+
const taskPlanContent = await loadFile(taskPlanAbs);
|
|
130
|
+
if (taskPlanContent) {
|
|
131
|
+
const mustHaves = parseTaskPlanMustHaves(taskPlanContent);
|
|
132
|
+
mustHaveCount = mustHaves.length;
|
|
133
|
+
if (mustHaveCount > 0 && summaryExists && summaryAbs) {
|
|
134
|
+
const summaryContent = await loadFile(summaryAbs);
|
|
135
|
+
if (summaryContent) {
|
|
136
|
+
mustHavesMentionedInSummary = countMustHavesMentionedInSummary(mustHaves, summaryContent);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
planPath,
|
|
144
|
+
summaryPath,
|
|
145
|
+
summaryExists,
|
|
146
|
+
taskChecked,
|
|
147
|
+
nextActionAdvanced,
|
|
148
|
+
mustHaveCount,
|
|
149
|
+
mustHavesMentionedInSummary,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function formatExecuteTaskRecoveryStatus(status: ExecuteTaskRecoveryStatus): string {
|
|
154
|
+
const missing = [] as string[];
|
|
155
|
+
if (!status.summaryExists) missing.push(`summary missing (${status.summaryPath})`);
|
|
156
|
+
if (!status.taskChecked) missing.push(`task checkbox unchecked in ${status.planPath}`);
|
|
157
|
+
if (!status.nextActionAdvanced) missing.push("state next action still points at the timed-out task");
|
|
158
|
+
if (status.mustHaveCount > 0 && status.mustHavesMentionedInSummary < status.mustHaveCount) {
|
|
159
|
+
missing.push(`must-have gap: ${status.mustHavesMentionedInSummary} of ${status.mustHaveCount} must-haves addressed in summary`);
|
|
160
|
+
}
|
|
161
|
+
return missing.length > 0 ? missing.join("; ") : "all durable task artifacts present";
|
|
162
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { readdirSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
import { loadFile, parsePlan, parseRoadmap } from "./files.ts";
|
|
5
|
+
import {
|
|
6
|
+
milestonesDir,
|
|
7
|
+
resolveMilestoneFile,
|
|
8
|
+
resolveSliceFile,
|
|
9
|
+
resolveSlicePath,
|
|
10
|
+
resolveTaskFile,
|
|
11
|
+
resolveTasksDir,
|
|
12
|
+
} from "./paths.ts";
|
|
13
|
+
import { deriveState } from "./state.ts";
|
|
14
|
+
import { type ValidationIssue, validateCompleteBoundary, validatePlanBoundary } from "./observability-validator.ts";
|
|
15
|
+
import { getSliceBranchName } from "./worktree.ts";
|
|
16
|
+
|
|
17
|
+
export interface WorkspaceTaskTarget {
|
|
18
|
+
id: string;
|
|
19
|
+
title: string;
|
|
20
|
+
done: boolean;
|
|
21
|
+
planPath?: string;
|
|
22
|
+
summaryPath?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface WorkspaceSliceTarget {
|
|
26
|
+
id: string;
|
|
27
|
+
title: string;
|
|
28
|
+
done: boolean;
|
|
29
|
+
planPath?: string;
|
|
30
|
+
summaryPath?: string;
|
|
31
|
+
uatPath?: string;
|
|
32
|
+
tasksDir?: string;
|
|
33
|
+
branch?: string;
|
|
34
|
+
tasks: WorkspaceTaskTarget[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface WorkspaceMilestoneTarget {
|
|
38
|
+
id: string;
|
|
39
|
+
title: string;
|
|
40
|
+
roadmapPath?: string;
|
|
41
|
+
slices: WorkspaceSliceTarget[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface WorkspaceScopeTarget {
|
|
45
|
+
scope: string;
|
|
46
|
+
label: string;
|
|
47
|
+
kind: "project" | "milestone" | "slice" | "task";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface GSDWorkspaceIndex {
|
|
51
|
+
milestones: WorkspaceMilestoneTarget[];
|
|
52
|
+
active: {
|
|
53
|
+
milestoneId?: string;
|
|
54
|
+
sliceId?: string;
|
|
55
|
+
taskId?: string;
|
|
56
|
+
phase: string;
|
|
57
|
+
};
|
|
58
|
+
scopes: WorkspaceScopeTarget[];
|
|
59
|
+
validationIssues: ValidationIssue[];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function findMilestoneIds(basePath: string): string[] {
|
|
63
|
+
try {
|
|
64
|
+
return readdirSync(milestonesDir(basePath), { withFileTypes: true })
|
|
65
|
+
.filter(entry => entry.isDirectory())
|
|
66
|
+
.map(entry => {
|
|
67
|
+
const match = entry.name.match(/^(M\d+)/);
|
|
68
|
+
return match ? match[1] : entry.name;
|
|
69
|
+
})
|
|
70
|
+
.sort();
|
|
71
|
+
} catch {
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function titleFromRoadmapHeader(content: string, fallbackId: string): string {
|
|
77
|
+
const roadmap = parseRoadmap(content);
|
|
78
|
+
return roadmap.title.replace(/^M\d+[^:]*:\s*/, "") || fallbackId;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function indexSlice(basePath: string, milestoneId: string, sliceId: string, fallbackTitle: string, done: boolean): Promise<WorkspaceSliceTarget> {
|
|
82
|
+
const planPath = resolveSliceFile(basePath, milestoneId, sliceId, "PLAN") ?? undefined;
|
|
83
|
+
const summaryPath = resolveSliceFile(basePath, milestoneId, sliceId, "SUMMARY") ?? undefined;
|
|
84
|
+
const uatPath = resolveSliceFile(basePath, milestoneId, sliceId, "UAT") ?? undefined;
|
|
85
|
+
const tasksDir = resolveTasksDir(basePath, milestoneId, sliceId) ?? undefined;
|
|
86
|
+
|
|
87
|
+
const tasks: WorkspaceTaskTarget[] = [];
|
|
88
|
+
let title = fallbackTitle;
|
|
89
|
+
|
|
90
|
+
if (planPath) {
|
|
91
|
+
const content = await loadFile(planPath);
|
|
92
|
+
if (content) {
|
|
93
|
+
const plan = parsePlan(content);
|
|
94
|
+
title = plan.title || fallbackTitle;
|
|
95
|
+
for (const task of plan.tasks) {
|
|
96
|
+
tasks.push({
|
|
97
|
+
id: task.id,
|
|
98
|
+
title: task.title,
|
|
99
|
+
done: task.done,
|
|
100
|
+
planPath: resolveTaskFile(basePath, milestoneId, sliceId, task.id, "PLAN") ?? undefined,
|
|
101
|
+
summaryPath: resolveTaskFile(basePath, milestoneId, sliceId, task.id, "SUMMARY") ?? undefined,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
id: sliceId,
|
|
109
|
+
title,
|
|
110
|
+
done,
|
|
111
|
+
planPath,
|
|
112
|
+
summaryPath,
|
|
113
|
+
uatPath,
|
|
114
|
+
tasksDir,
|
|
115
|
+
branch: getSliceBranchName(milestoneId, sliceId),
|
|
116
|
+
tasks,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function indexWorkspace(basePath: string): Promise<GSDWorkspaceIndex> {
|
|
121
|
+
const milestoneIds = findMilestoneIds(basePath);
|
|
122
|
+
const milestones: WorkspaceMilestoneTarget[] = [];
|
|
123
|
+
const validationIssues: ValidationIssue[] = [];
|
|
124
|
+
|
|
125
|
+
for (const milestoneId of milestoneIds) {
|
|
126
|
+
const roadmapPath = resolveMilestoneFile(basePath, milestoneId, "ROADMAP") ?? undefined;
|
|
127
|
+
let title = milestoneId;
|
|
128
|
+
const slices: WorkspaceSliceTarget[] = [];
|
|
129
|
+
|
|
130
|
+
if (roadmapPath) {
|
|
131
|
+
const roadmapContent = await loadFile(roadmapPath);
|
|
132
|
+
if (roadmapContent) {
|
|
133
|
+
const roadmap = parseRoadmap(roadmapContent);
|
|
134
|
+
title = titleFromRoadmapHeader(roadmapContent, milestoneId);
|
|
135
|
+
for (const slice of roadmap.slices) {
|
|
136
|
+
const indexedSlice = await indexSlice(basePath, milestoneId, slice.id, slice.title, slice.done);
|
|
137
|
+
slices.push(indexedSlice);
|
|
138
|
+
validationIssues.push(...await validatePlanBoundary(basePath, milestoneId, slice.id));
|
|
139
|
+
validationIssues.push(...await validateCompleteBoundary(basePath, milestoneId, slice.id));
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
milestones.push({ id: milestoneId, title, roadmapPath, slices });
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const state = await deriveState(basePath);
|
|
148
|
+
const active = {
|
|
149
|
+
milestoneId: state.activeMilestone?.id,
|
|
150
|
+
sliceId: state.activeSlice?.id,
|
|
151
|
+
taskId: state.activeTask?.id,
|
|
152
|
+
phase: state.phase,
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const scopes: WorkspaceScopeTarget[] = [{ scope: "project", label: "project", kind: "project" }];
|
|
156
|
+
for (const milestone of milestones) {
|
|
157
|
+
scopes.push({ scope: milestone.id, label: `${milestone.id}: ${milestone.title}`, kind: "milestone" });
|
|
158
|
+
for (const slice of milestone.slices) {
|
|
159
|
+
scopes.push({ scope: `${milestone.id}/${slice.id}`, label: `${milestone.id}/${slice.id}: ${slice.title}`, kind: "slice" });
|
|
160
|
+
for (const task of slice.tasks) {
|
|
161
|
+
scopes.push({
|
|
162
|
+
scope: `${milestone.id}/${slice.id}/${task.id}`,
|
|
163
|
+
label: `${milestone.id}/${slice.id}/${task.id}: ${task.title}`,
|
|
164
|
+
kind: "task",
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return { milestones, active, scopes, validationIssues };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export async function listDoctorScopeSuggestions(basePath: string): Promise<Array<{ value: string; label: string }>> {
|
|
174
|
+
const index = await indexWorkspace(basePath);
|
|
175
|
+
const activeSliceScope = index.active.milestoneId && index.active.sliceId
|
|
176
|
+
? `${index.active.milestoneId}/${index.active.sliceId}`
|
|
177
|
+
: null;
|
|
178
|
+
|
|
179
|
+
const ordered = [...index.scopes].filter(scope => scope.kind !== "project");
|
|
180
|
+
ordered.sort((a, b) => {
|
|
181
|
+
if (activeSliceScope && a.scope === activeSliceScope) return -1;
|
|
182
|
+
if (activeSliceScope && b.scope === activeSliceScope) return 1;
|
|
183
|
+
return a.scope.localeCompare(b.scope);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
return ordered.map(scope => ({ value: scope.scope, label: scope.label }));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export async function getSuggestedNextCommands(basePath: string): Promise<string[]> {
|
|
190
|
+
const index = await indexWorkspace(basePath);
|
|
191
|
+
const scope = index.active.milestoneId && index.active.sliceId
|
|
192
|
+
? `${index.active.milestoneId}/${index.active.sliceId}`
|
|
193
|
+
: index.active.milestoneId;
|
|
194
|
+
|
|
195
|
+
const commands = new Set<string>();
|
|
196
|
+
if (index.active.phase === "planning") commands.add("/gsd");
|
|
197
|
+
if (index.active.phase === "executing" || index.active.phase === "summarizing") commands.add("/gsd auto");
|
|
198
|
+
if (scope) commands.add(`/gsd doctor ${scope}`);
|
|
199
|
+
if (scope) commands.add(`/gsd doctor fix ${scope}`);
|
|
200
|
+
if (index.validationIssues.length > 0 && scope) commands.add(`/gsd doctor audit ${scope}`);
|
|
201
|
+
commands.add("/gsd status");
|
|
202
|
+
return [...commands];
|
|
203
|
+
}
|