gsd-pi 2.17.0 → 2.18.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 +39 -0
- package/dist/onboarding.js +2 -2
- package/dist/remote-questions-config.d.ts +10 -0
- package/dist/remote-questions-config.js +36 -0
- package/dist/resources/extensions/gsd/activity-log.ts +37 -7
- package/dist/resources/extensions/gsd/auto-prompts.ts +20 -1
- package/dist/resources/extensions/gsd/auto-worktree.ts +33 -4
- package/dist/resources/extensions/gsd/auto.ts +123 -10
- package/dist/resources/extensions/gsd/commands.ts +245 -22
- package/dist/resources/extensions/gsd/dispatch-guard.ts +7 -19
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +201 -2
- package/dist/resources/extensions/gsd/files.ts +123 -1
- package/dist/resources/extensions/gsd/guided-flow.ts +237 -4
- package/dist/resources/extensions/gsd/index.ts +47 -3
- package/dist/resources/extensions/gsd/paths.ts +9 -0
- package/dist/resources/extensions/gsd/preferences.ts +59 -1
- package/dist/resources/extensions/gsd/prompts/execute-task.md +6 -5
- package/dist/resources/extensions/gsd/prompts/system.md +2 -0
- package/dist/resources/extensions/gsd/queue-order.ts +231 -0
- package/dist/resources/extensions/gsd/queue-reorder-ui.ts +263 -0
- package/dist/resources/extensions/gsd/state.ts +15 -3
- package/dist/resources/extensions/gsd/templates/knowledge.md +19 -0
- package/dist/resources/extensions/gsd/templates/preferences.md +14 -0
- package/dist/resources/extensions/gsd/tests/auto-worktree.test.ts +20 -0
- package/dist/resources/extensions/gsd/tests/derive-state-deps.test.ts +99 -0
- package/dist/resources/extensions/gsd/tests/in-flight-tool-tracking.test.ts +79 -0
- package/dist/resources/extensions/gsd/tests/knowledge.test.ts +161 -0
- package/dist/resources/extensions/gsd/tests/memory-leak-guards.test.ts +87 -0
- package/dist/resources/extensions/gsd/tests/preferences-wizard-fields.test.ts +168 -0
- package/dist/resources/extensions/gsd/tests/queue-order.test.ts +204 -0
- package/dist/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +281 -0
- package/dist/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +139 -0
- package/dist/resources/extensions/gsd/worktree-manager.ts +8 -5
- package/dist/resources/extensions/gsd/worktree.ts +22 -0
- package/dist/resources/extensions/shared/next-action-ui.ts +16 -1
- package/package.json +1 -1
- package/packages/pi-coding-agent/dist/cli/args.d.ts +5 -0
- package/packages/pi-coding-agent/dist/cli/args.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/cli/args.js +21 -0
- package/packages/pi-coding-agent/dist/cli/args.js.map +1 -1
- package/packages/pi-coding-agent/dist/cli/list-models.d.ts +14 -3
- package/packages/pi-coding-agent/dist/cli/list-models.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/cli/list-models.js +52 -17
- package/packages/pi-coding-agent/dist/cli/list-models.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/discovery-cache.d.ts +27 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.js +79 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.test.js +140 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.d.ts +35 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.js +162 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.test.js +100 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.js +113 -0
- package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-registry.d.ts +26 -0
- package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-registry.js +98 -0
- package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/models-json-writer.d.ts +62 -0
- package/packages/pi-coding-agent/dist/core/models-json-writer.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/models-json-writer.js +145 -0
- package/packages/pi-coding-agent/dist/core/models-json-writer.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/models-json-writer.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/models-json-writer.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/models-json-writer.test.js +118 -0
- package/packages/pi-coding-agent/dist/core/models-json-writer.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +9 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.js +11 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/slash-commands.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/slash-commands.js +1 -0
- package/packages/pi-coding-agent/dist/core/slash-commands.js.map +1 -1
- package/packages/pi-coding-agent/dist/index.d.ts +5 -1
- package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/index.js +4 -1
- package/packages/pi-coding-agent/dist/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/main.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/main.js +17 -2
- package/packages/pi-coding-agent/dist/main.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/index.d.ts +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/index.js +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.d.ts +25 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js +121 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +32 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/src/cli/args.ts +21 -0
- package/packages/pi-coding-agent/src/cli/list-models.ts +70 -17
- package/packages/pi-coding-agent/src/core/discovery-cache.test.ts +170 -0
- package/packages/pi-coding-agent/src/core/discovery-cache.ts +97 -0
- package/packages/pi-coding-agent/src/core/model-discovery.test.ts +125 -0
- package/packages/pi-coding-agent/src/core/model-discovery.ts +231 -0
- package/packages/pi-coding-agent/src/core/model-registry-discovery.test.ts +135 -0
- package/packages/pi-coding-agent/src/core/model-registry.ts +107 -0
- package/packages/pi-coding-agent/src/core/models-json-writer.test.ts +145 -0
- package/packages/pi-coding-agent/src/core/models-json-writer.ts +188 -0
- package/packages/pi-coding-agent/src/core/settings-manager.ts +21 -0
- package/packages/pi-coding-agent/src/core/slash-commands.ts +1 -0
- package/packages/pi-coding-agent/src/index.ts +5 -0
- package/packages/pi-coding-agent/src/main.ts +19 -2
- package/packages/pi-coding-agent/src/modes/interactive/components/index.ts +1 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/model-selector.ts +1 -1
- package/packages/pi-coding-agent/src/modes/interactive/components/provider-manager.ts +163 -0
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +37 -0
- package/src/resources/extensions/gsd/activity-log.ts +37 -7
- package/src/resources/extensions/gsd/auto-prompts.ts +20 -1
- package/src/resources/extensions/gsd/auto-worktree.ts +33 -4
- package/src/resources/extensions/gsd/auto.ts +123 -10
- package/src/resources/extensions/gsd/commands.ts +245 -22
- package/src/resources/extensions/gsd/dispatch-guard.ts +7 -19
- package/src/resources/extensions/gsd/docs/preferences-reference.md +201 -2
- package/src/resources/extensions/gsd/files.ts +123 -1
- package/src/resources/extensions/gsd/guided-flow.ts +237 -4
- package/src/resources/extensions/gsd/index.ts +47 -3
- package/src/resources/extensions/gsd/paths.ts +9 -0
- package/src/resources/extensions/gsd/preferences.ts +59 -1
- package/src/resources/extensions/gsd/prompts/execute-task.md +6 -5
- package/src/resources/extensions/gsd/prompts/system.md +2 -0
- package/src/resources/extensions/gsd/queue-order.ts +231 -0
- package/src/resources/extensions/gsd/queue-reorder-ui.ts +263 -0
- package/src/resources/extensions/gsd/state.ts +15 -3
- package/src/resources/extensions/gsd/templates/knowledge.md +19 -0
- package/src/resources/extensions/gsd/templates/preferences.md +14 -0
- package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +20 -0
- package/src/resources/extensions/gsd/tests/derive-state-deps.test.ts +99 -0
- package/src/resources/extensions/gsd/tests/in-flight-tool-tracking.test.ts +79 -0
- package/src/resources/extensions/gsd/tests/knowledge.test.ts +161 -0
- package/src/resources/extensions/gsd/tests/memory-leak-guards.test.ts +87 -0
- package/src/resources/extensions/gsd/tests/preferences-wizard-fields.test.ts +168 -0
- package/src/resources/extensions/gsd/tests/queue-order.test.ts +204 -0
- package/src/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +281 -0
- package/src/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +139 -0
- package/src/resources/extensions/gsd/worktree-manager.ts +8 -5
- package/src/resources/extensions/gsd/worktree.ts +22 -0
- package/src/resources/extensions/shared/next-action-ui.ts +16 -1
package/README.md
CHANGED
|
@@ -21,6 +21,25 @@ One command. Walk away. Come back to a built project with clean git history.
|
|
|
21
21
|
|
|
22
22
|
---
|
|
23
23
|
|
|
24
|
+
## Documentation
|
|
25
|
+
|
|
26
|
+
Full documentation is available in the [`docs/`](./docs/) directory:
|
|
27
|
+
|
|
28
|
+
- **[Getting Started](./docs/getting-started.md)** — install, first run, basic usage
|
|
29
|
+
- **[Auto Mode](./docs/auto-mode.md)** — autonomous execution deep-dive
|
|
30
|
+
- **[Configuration](./docs/configuration.md)** — all preferences, models, git, and hooks
|
|
31
|
+
- **[Token Optimization](./docs/token-optimization.md)** — profiles, context compression, complexity routing (v2.17)
|
|
32
|
+
- **[Cost Management](./docs/cost-management.md)** — budgets, tracking, projections
|
|
33
|
+
- **[Git Strategy](./docs/git-strategy.md)** — worktree isolation, branching, merge behavior
|
|
34
|
+
- **[Working in Teams](./docs/working-in-teams.md)** — unique IDs, shared artifacts
|
|
35
|
+
- **[Skills](./docs/skills.md)** — bundled skills, discovery, custom authoring
|
|
36
|
+
- **[Commands Reference](./docs/commands.md)** — all commands and keyboard shortcuts
|
|
37
|
+
- **[Architecture](./docs/architecture.md)** — system design and dispatch pipeline
|
|
38
|
+
- **[Troubleshooting](./docs/troubleshooting.md)** — common issues, doctor, recovery
|
|
39
|
+
- **[Migration from v1](./docs/migration.md)** — `.planning` → `.gsd` migration
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
24
43
|
## What Changed From v1
|
|
25
44
|
|
|
26
45
|
The original GSD was a collection of markdown prompts installed into `~/.claude/commands/`. It relied entirely on the LLM reading those prompts and doing the right thing. That worked surprisingly well — but it had hard limits:
|
|
@@ -334,6 +353,26 @@ unique_milestone_ids: true
|
|
|
334
353
|
| `skill_rules` | Situational rules for skill routing |
|
|
335
354
|
| `unique_milestone_ids` | Uses unique milestone names to avoid clashes when working in teams of people |
|
|
336
355
|
|
|
356
|
+
### Token Optimization (v2.17)
|
|
357
|
+
|
|
358
|
+
GSD 2.17 introduced a coordinated token optimization system that reduces usage by 40-60% on cost-sensitive workloads. Set a single preference to coordinate model selection, phase skipping, and context compression:
|
|
359
|
+
|
|
360
|
+
```yaml
|
|
361
|
+
token_profile: budget # or balanced (default), quality
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
| Profile | Savings | What It Does |
|
|
365
|
+
|---------|---------|-------------|
|
|
366
|
+
| `budget` | 40-60% | Cheap models, skip research/reassess, minimal context inlining |
|
|
367
|
+
| `balanced` | 10-20% | Default models, skip slice research, standard context |
|
|
368
|
+
| `quality` | 0% | All phases, all context, full model power |
|
|
369
|
+
|
|
370
|
+
**Complexity-based routing** automatically classifies tasks as simple/standard/complex and routes to appropriate models. Simple docs tasks get Haiku; complex architectural work gets Opus. The classification is heuristic (sub-millisecond, no LLM calls) and learns from outcomes via a persistent routing history.
|
|
371
|
+
|
|
372
|
+
**Budget pressure** graduates model downgrading as you approach your budget ceiling — 50%, 75%, and 90% thresholds progressively shift work to cheaper tiers.
|
|
373
|
+
|
|
374
|
+
See the full [Token Optimization Guide](./docs/token-optimization.md) for details.
|
|
375
|
+
|
|
337
376
|
### Bundled Tools
|
|
338
377
|
|
|
339
378
|
GSD ships with 14 extensions, all loaded automatically:
|
package/dist/onboarding.js
CHANGED
|
@@ -633,7 +633,7 @@ async function runRemoteQuestionsStep(p, pc, authStorage) {
|
|
|
633
633
|
});
|
|
634
634
|
if (p.isCancel(channelId) || !channelId)
|
|
635
635
|
return null;
|
|
636
|
-
const { saveRemoteQuestionsConfig } = await import('./
|
|
636
|
+
const { saveRemoteQuestionsConfig } = await import('./remote-questions-config.js');
|
|
637
637
|
saveRemoteQuestionsConfig('slack', channelId.trim());
|
|
638
638
|
p.log.success(`Slack channel: ${pc.green(channelId.trim())}`);
|
|
639
639
|
return 'Slack';
|
|
@@ -736,7 +736,7 @@ async function runDiscordChannelStep(p, pc, token) {
|
|
|
736
736
|
channelId = channelChoice;
|
|
737
737
|
}
|
|
738
738
|
// Save remote questions config
|
|
739
|
-
const { saveRemoteQuestionsConfig } = await import('./
|
|
739
|
+
const { saveRemoteQuestionsConfig } = await import('./remote-questions-config.js');
|
|
740
740
|
saveRemoteQuestionsConfig('discord', channelId);
|
|
741
741
|
const channelName = channels.find(ch => ch.id === channelId)?.name;
|
|
742
742
|
p.log.success(`Discord channel: ${pc.green(channelName ? `#${channelName}` : channelId)}`);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remote Questions Config Helper
|
|
3
|
+
*
|
|
4
|
+
* Extracted from remote-questions extension so onboarding.ts can import
|
|
5
|
+
* it without crossing the compiled/uncompiled boundary. The extension
|
|
6
|
+
* files in src/resources/ are shipped as raw .ts and loaded via jiti,
|
|
7
|
+
* but onboarding.ts is compiled by tsc — dynamic imports from compiled
|
|
8
|
+
* JS to uncompiled .ts fail at runtime (#592).
|
|
9
|
+
*/
|
|
10
|
+
export declare function saveRemoteQuestionsConfig(channel: "slack" | "discord", channelId: string): void;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remote Questions Config Helper
|
|
3
|
+
*
|
|
4
|
+
* Extracted from remote-questions extension so onboarding.ts can import
|
|
5
|
+
* it without crossing the compiled/uncompiled boundary. The extension
|
|
6
|
+
* files in src/resources/ are shipped as raw .ts and loaded via jiti,
|
|
7
|
+
* but onboarding.ts is compiled by tsc — dynamic imports from compiled
|
|
8
|
+
* JS to uncompiled .ts fail at runtime (#592).
|
|
9
|
+
*/
|
|
10
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
11
|
+
import { dirname } from "node:path";
|
|
12
|
+
import { getGlobalGSDPreferencesPath } from "./resources/extensions/gsd/preferences.js";
|
|
13
|
+
export function saveRemoteQuestionsConfig(channel, channelId) {
|
|
14
|
+
const prefsPath = getGlobalGSDPreferencesPath();
|
|
15
|
+
const block = [
|
|
16
|
+
"remote_questions:",
|
|
17
|
+
` channel: ${channel}`,
|
|
18
|
+
` channel_id: "${channelId}"`,
|
|
19
|
+
" timeout_minutes: 5",
|
|
20
|
+
" poll_interval_seconds: 5",
|
|
21
|
+
].join("\n");
|
|
22
|
+
const content = existsSync(prefsPath) ? readFileSync(prefsPath, "utf-8") : "";
|
|
23
|
+
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
24
|
+
let next = content;
|
|
25
|
+
if (fmMatch) {
|
|
26
|
+
let frontmatter = fmMatch[1];
|
|
27
|
+
const regex = /remote_questions:[\s\S]*?(?=\n[a-zA-Z_]|\n---|$)/;
|
|
28
|
+
frontmatter = regex.test(frontmatter) ? frontmatter.replace(regex, block) : `${frontmatter.trimEnd()}\n${block}`;
|
|
29
|
+
next = `---\n${frontmatter}\n---${content.slice(fmMatch[0].length)}`;
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
next = `---\n${block}\n---\n\n${content}`;
|
|
33
|
+
}
|
|
34
|
+
mkdirSync(dirname(prefsPath), { recursive: true });
|
|
35
|
+
writeFileSync(prefsPath, next, "utf-8");
|
|
36
|
+
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* Diagnostic extraction is handled by session-forensics.ts.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { writeFileSync, mkdirSync, readdirSync, unlinkSync, statSync, openSync, closeSync, constants } from "node:fs";
|
|
11
|
+
import { writeFileSync, writeSync, mkdirSync, readdirSync, unlinkSync, statSync, openSync, closeSync, constants } from "node:fs";
|
|
12
12
|
import { createHash } from "node:crypto";
|
|
13
13
|
import { join } from "node:path";
|
|
14
14
|
|
|
@@ -23,6 +23,15 @@ interface ActivityLogState {
|
|
|
23
23
|
|
|
24
24
|
const activityLogState = new Map<string, ActivityLogState>();
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Clear accumulated activity log state (#611).
|
|
28
|
+
* Call when auto-mode stops to prevent unbounded memory growth
|
|
29
|
+
* from lastSnapshotKeyByUnit maps accumulating across units.
|
|
30
|
+
*/
|
|
31
|
+
export function clearActivityLogState(): void {
|
|
32
|
+
activityLogState.clear();
|
|
33
|
+
}
|
|
34
|
+
|
|
26
35
|
function scanNextSequence(activityDir: string): number {
|
|
27
36
|
let maxSeq = 0;
|
|
28
37
|
try {
|
|
@@ -46,9 +55,21 @@ function getActivityState(activityDir: string): ActivityLogState {
|
|
|
46
55
|
return state;
|
|
47
56
|
}
|
|
48
57
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
58
|
+
/**
|
|
59
|
+
* Build a lightweight dedup key from session entries without serializing
|
|
60
|
+
* the entire content to a string (#611). Uses entry count + hash of
|
|
61
|
+
* the last few entries as a fingerprint instead of hashing megabytes.
|
|
62
|
+
*/
|
|
63
|
+
function snapshotKey(unitType: string, unitId: string, entries: unknown[]): string {
|
|
64
|
+
const hash = createHash("sha1");
|
|
65
|
+
hash.update(`${unitType}\0${unitId}\0${entries.length}\0`);
|
|
66
|
+
// Hash only the last 3 entries as a fingerprint — if the session grew,
|
|
67
|
+
// the count change alone detects it; if content changed, the tail hash catches it.
|
|
68
|
+
const tail = entries.slice(-3);
|
|
69
|
+
for (const entry of tail) {
|
|
70
|
+
hash.update(JSON.stringify(entry));
|
|
71
|
+
}
|
|
72
|
+
return hash.digest("hex");
|
|
52
73
|
}
|
|
53
74
|
|
|
54
75
|
function nextActivityFilePath(
|
|
@@ -91,14 +112,23 @@ export function saveActivityLog(
|
|
|
91
112
|
mkdirSync(activityDir, { recursive: true });
|
|
92
113
|
|
|
93
114
|
const safeUnitId = unitId.replace(/\//g, "-");
|
|
94
|
-
const content = `${entries.map(entry => JSON.stringify(entry)).join("\n")}\n`;
|
|
95
115
|
const state = getActivityState(activityDir);
|
|
96
116
|
const unitKey = `${unitType}\0${safeUnitId}`;
|
|
97
|
-
|
|
117
|
+
// Use lightweight fingerprint instead of serializing all entries (#611)
|
|
118
|
+
const key = snapshotKey(unitType, safeUnitId, entries);
|
|
98
119
|
if (state.lastSnapshotKeyByUnit.get(unitKey) === key) return;
|
|
99
120
|
|
|
100
121
|
const filePath = nextActivityFilePath(activityDir, state, unitType, safeUnitId);
|
|
101
|
-
|
|
122
|
+
// Stream entries to disk line-by-line instead of building one massive string (#611).
|
|
123
|
+
// For large sessions, the single-string approach allocated hundreds of MB.
|
|
124
|
+
const fd = openSync(filePath, "w");
|
|
125
|
+
try {
|
|
126
|
+
for (const entry of entries) {
|
|
127
|
+
writeSync(fd, JSON.stringify(entry) + "\n");
|
|
128
|
+
}
|
|
129
|
+
} finally {
|
|
130
|
+
closeSync(fd);
|
|
131
|
+
}
|
|
102
132
|
state.nextSeq += 1;
|
|
103
133
|
state.lastSnapshotKeyByUnit.set(unitKey, key);
|
|
104
134
|
} catch (e) {
|
|
@@ -89,7 +89,7 @@ export async function inlineDependencySummaries(
|
|
|
89
89
|
export async function inlineGsdRootFile(
|
|
90
90
|
base: string, filename: string, label: string,
|
|
91
91
|
): Promise<string | null> {
|
|
92
|
-
const key = filename.replace(/\.md$/i, "").toUpperCase() as "PROJECT" | "DECISIONS" | "QUEUE" | "STATE" | "REQUIREMENTS";
|
|
92
|
+
const key = filename.replace(/\.md$/i, "").toUpperCase() as "PROJECT" | "DECISIONS" | "QUEUE" | "STATE" | "REQUIREMENTS" | "KNOWLEDGE";
|
|
93
93
|
const absPath = resolveGsdRootFile(base, key);
|
|
94
94
|
if (!existsSync(absPath)) return null;
|
|
95
95
|
return inlineFileOptional(absPath, relGsdRootFile(key), label);
|
|
@@ -377,6 +377,8 @@ export async function buildResearchMilestonePrompt(mid: string, midTitle: string
|
|
|
377
377
|
if (requirementsInline) inlined.push(requirementsInline);
|
|
378
378
|
const decisionsInline = await inlineGsdRootFile(base, "decisions.md", "Decisions");
|
|
379
379
|
if (decisionsInline) inlined.push(decisionsInline);
|
|
380
|
+
const knowledgeInlineRM = await inlineGsdRootFile(base, "knowledge.md", "Project Knowledge");
|
|
381
|
+
if (knowledgeInlineRM) inlined.push(knowledgeInlineRM);
|
|
380
382
|
inlined.push(inlineTemplate("research", "Research"));
|
|
381
383
|
|
|
382
384
|
const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
|
|
@@ -413,6 +415,8 @@ export async function buildPlanMilestonePrompt(mid: string, midTitle: string, ba
|
|
|
413
415
|
if (requirementsInline) inlined.push(requirementsInline);
|
|
414
416
|
const decisionsInline = inlineLevel !== "minimal" ? await inlineGsdRootFile(base, "decisions.md", "Decisions") : null;
|
|
415
417
|
if (decisionsInline) inlined.push(decisionsInline);
|
|
418
|
+
const knowledgeInlinePM = await inlineGsdRootFile(base, "knowledge.md", "Project Knowledge");
|
|
419
|
+
if (knowledgeInlinePM) inlined.push(knowledgeInlinePM);
|
|
416
420
|
inlined.push(inlineTemplate("roadmap", "Roadmap"));
|
|
417
421
|
if (inlineLevel === "full") {
|
|
418
422
|
inlined.push(inlineTemplate("decisions", "Decisions"));
|
|
@@ -461,6 +465,8 @@ export async function buildResearchSlicePrompt(
|
|
|
461
465
|
if (decisionsInline) inlined.push(decisionsInline);
|
|
462
466
|
const requirementsInline = await inlineGsdRootFile(base, "requirements.md", "Requirements");
|
|
463
467
|
if (requirementsInline) inlined.push(requirementsInline);
|
|
468
|
+
const knowledgeInlineRS = await inlineGsdRootFile(base, "knowledge.md", "Project Knowledge");
|
|
469
|
+
if (knowledgeInlineRS) inlined.push(knowledgeInlineRS);
|
|
464
470
|
inlined.push(inlineTemplate("research", "Research"));
|
|
465
471
|
|
|
466
472
|
const depContent = await inlineDependencySummaries(mid, sid, base);
|
|
@@ -504,6 +510,8 @@ export async function buildPlanSlicePrompt(
|
|
|
504
510
|
const requirementsInline = await inlineGsdRootFile(base, "requirements.md", "Requirements");
|
|
505
511
|
if (requirementsInline) inlined.push(requirementsInline);
|
|
506
512
|
}
|
|
513
|
+
const knowledgeInlinePS = await inlineGsdRootFile(base, "knowledge.md", "Project Knowledge");
|
|
514
|
+
if (knowledgeInlinePS) inlined.push(knowledgeInlinePS);
|
|
507
515
|
inlined.push(inlineTemplate("plan", "Slice Plan"));
|
|
508
516
|
if (inlineLevel === "full") {
|
|
509
517
|
inlined.push(inlineTemplate("task-plan", "Task Plan"));
|
|
@@ -578,11 +586,16 @@ export async function buildExecuteTaskPrompt(
|
|
|
578
586
|
? priorSummaries.slice(-1)
|
|
579
587
|
: priorSummaries;
|
|
580
588
|
const carryForwardSection = await buildCarryForwardSection(effectivePriorSummaries, base);
|
|
589
|
+
|
|
590
|
+
// Inline project knowledge if available
|
|
591
|
+
const knowledgeInlineET = await inlineGsdRootFile(base, "knowledge.md", "Project Knowledge");
|
|
592
|
+
|
|
581
593
|
const inlinedTemplates = inlineLevel === "minimal"
|
|
582
594
|
? inlineTemplate("task-summary", "Task Summary")
|
|
583
595
|
: [
|
|
584
596
|
inlineTemplate("task-summary", "Task Summary"),
|
|
585
597
|
inlineTemplate("decisions", "Decisions"),
|
|
598
|
+
...(knowledgeInlineET ? [knowledgeInlineET] : []),
|
|
586
599
|
].join("\n\n---\n\n");
|
|
587
600
|
|
|
588
601
|
const taskSummaryPath = `${relSlicePath(base, mid, sid)}/tasks/${tid}-SUMMARY.md`;
|
|
@@ -624,6 +637,8 @@ export async function buildCompleteSlicePrompt(
|
|
|
624
637
|
const requirementsInline = await inlineGsdRootFile(base, "requirements.md", "Requirements");
|
|
625
638
|
if (requirementsInline) inlined.push(requirementsInline);
|
|
626
639
|
}
|
|
640
|
+
const knowledgeInlineCS = await inlineGsdRootFile(base, "knowledge.md", "Project Knowledge");
|
|
641
|
+
if (knowledgeInlineCS) inlined.push(knowledgeInlineCS);
|
|
627
642
|
|
|
628
643
|
// Inline all task summaries for this slice
|
|
629
644
|
const tDir = resolveTasksDir(base, mid, sid);
|
|
@@ -697,6 +712,8 @@ export async function buildCompleteMilestonePrompt(
|
|
|
697
712
|
const projectInline = await inlineGsdRootFile(base, "project.md", "Project");
|
|
698
713
|
if (projectInline) inlined.push(projectInline);
|
|
699
714
|
}
|
|
715
|
+
const knowledgeInlineCM = await inlineGsdRootFile(base, "knowledge.md", "Project Knowledge");
|
|
716
|
+
if (knowledgeInlineCM) inlined.push(knowledgeInlineCM);
|
|
700
717
|
// Inline milestone context file (milestone-level, not GSD root)
|
|
701
718
|
const contextPath = resolveMilestoneFile(base, mid, "CONTEXT");
|
|
702
719
|
const contextRel = relMilestoneFile(base, mid, "CONTEXT");
|
|
@@ -825,6 +842,8 @@ export async function buildReassessRoadmapPrompt(
|
|
|
825
842
|
const decisionsInline = await inlineGsdRootFile(base, "decisions.md", "Decisions");
|
|
826
843
|
if (decisionsInline) inlined.push(decisionsInline);
|
|
827
844
|
}
|
|
845
|
+
const knowledgeInlineRA = await inlineGsdRootFile(base, "knowledge.md", "Project Knowledge");
|
|
846
|
+
if (knowledgeInlineRA) inlined.push(knowledgeInlineRA);
|
|
828
847
|
|
|
829
848
|
const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
|
|
830
849
|
|
|
@@ -14,8 +14,10 @@ import {
|
|
|
14
14
|
removeWorktree,
|
|
15
15
|
worktreePath,
|
|
16
16
|
} from "./worktree-manager.js";
|
|
17
|
+
import { detectWorktreeName } from "./worktree.js";
|
|
17
18
|
import {
|
|
18
19
|
MergeConflictError,
|
|
20
|
+
readIntegrationBranch,
|
|
19
21
|
} from "./git-service.js";
|
|
20
22
|
import { parseRoadmap } from "./files.js";
|
|
21
23
|
import { loadEffectiveGSDPreferences } from "./preferences.js";
|
|
@@ -90,7 +92,12 @@ export function autoWorktreeBranch(milestoneId: string): string {
|
|
|
90
92
|
*/
|
|
91
93
|
export function createAutoWorktree(basePath: string, milestoneId: string): string {
|
|
92
94
|
const branch = autoWorktreeBranch(milestoneId);
|
|
93
|
-
|
|
95
|
+
|
|
96
|
+
// Use the integration branch recorded in META.json as the start point.
|
|
97
|
+
// This ensures the worktree branch is created from the branch the user
|
|
98
|
+
// was on when they started the milestone (e.g. f-setup-gsd-2), not main.
|
|
99
|
+
const integrationBranch = readIntegrationBranch(basePath, milestoneId) ?? undefined;
|
|
100
|
+
const info = createWorktree(basePath, milestoneId, { branch, startPoint: integrationBranch });
|
|
94
101
|
|
|
95
102
|
// Copy .gsd/ planning artifacts from the source repo into the new worktree.
|
|
96
103
|
// Worktrees are fresh git checkouts — untracked files don't carry over.
|
|
@@ -224,6 +231,27 @@ export function getAutoWorktreeOriginalBase(): string | null {
|
|
|
224
231
|
return originalBase;
|
|
225
232
|
}
|
|
226
233
|
|
|
234
|
+
export function getActiveAutoWorktreeContext(): {
|
|
235
|
+
originalBase: string;
|
|
236
|
+
worktreeName: string;
|
|
237
|
+
branch: string;
|
|
238
|
+
} | null {
|
|
239
|
+
if (!originalBase) return null;
|
|
240
|
+
const cwd = process.cwd();
|
|
241
|
+
const resolvedBase = existsSync(originalBase) ? realpathSync(originalBase) : originalBase;
|
|
242
|
+
const wtDir = join(resolvedBase, ".gsd", "worktrees");
|
|
243
|
+
if (!cwd.startsWith(wtDir)) return null;
|
|
244
|
+
const worktreeName = detectWorktreeName(cwd);
|
|
245
|
+
if (!worktreeName) return null;
|
|
246
|
+
const branch = nativeGetCurrentBranch(cwd);
|
|
247
|
+
if (!branch.startsWith("milestone/")) return null;
|
|
248
|
+
return {
|
|
249
|
+
originalBase,
|
|
250
|
+
worktreeName,
|
|
251
|
+
branch,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
227
255
|
// ─── Merge Milestone -> Main ───────────────────────────────────────────────
|
|
228
256
|
|
|
229
257
|
/**
|
|
@@ -279,11 +307,12 @@ export function mergeMilestoneToMain(
|
|
|
279
307
|
const previousCwd = process.cwd();
|
|
280
308
|
process.chdir(originalBasePath_);
|
|
281
309
|
|
|
282
|
-
// 4. Resolve
|
|
310
|
+
// 4. Resolve integration branch — prefer milestone metadata, fall back to preferences / "main"
|
|
283
311
|
const prefs = loadEffectiveGSDPreferences()?.preferences?.git ?? {};
|
|
284
|
-
const
|
|
312
|
+
const integrationBranch = readIntegrationBranch(originalBasePath_, milestoneId);
|
|
313
|
+
const mainBranch = integrationBranch ?? prefs.main_branch ?? "main";
|
|
285
314
|
|
|
286
|
-
// 5. Checkout
|
|
315
|
+
// 5. Checkout integration branch
|
|
287
316
|
nativeCheckoutBranch(originalBasePath_, mainBranch);
|
|
288
317
|
|
|
289
318
|
// 6. Build rich commit message
|
|
@@ -29,7 +29,7 @@ import {
|
|
|
29
29
|
buildMilestoneFileName, buildSliceFileName, buildTaskFileName,
|
|
30
30
|
} from "./paths.js";
|
|
31
31
|
import { invalidateAllCaches } from "./cache.js";
|
|
32
|
-
import { saveActivityLog } from "./activity-log.js";
|
|
32
|
+
import { saveActivityLog, clearActivityLogState } from "./activity-log.js";
|
|
33
33
|
import { synthesizeCrashRecovery, getDeepDiagnostic } from "./session-forensics.js";
|
|
34
34
|
import { writeLock, clearLock, readCrashLock, formatCrashInfo, isLockProcessAlive } from "./crash-recovery.js";
|
|
35
35
|
import {
|
|
@@ -92,7 +92,9 @@ import {
|
|
|
92
92
|
getAutoWorktreePath,
|
|
93
93
|
getAutoWorktreeOriginalBase,
|
|
94
94
|
mergeMilestoneToMain,
|
|
95
|
+
autoWorktreeBranch,
|
|
95
96
|
} from "./auto-worktree.js";
|
|
97
|
+
import { pruneQueueOrder } from "./queue-order.js";
|
|
96
98
|
import { showNextAction } from "../shared/next-action-ui.js";
|
|
97
99
|
import {
|
|
98
100
|
resolveExpectedArtifactPath,
|
|
@@ -196,6 +198,33 @@ function shouldUseWorktreeIsolation(): boolean {
|
|
|
196
198
|
return true; // default: worktree
|
|
197
199
|
}
|
|
198
200
|
|
|
201
|
+
/**
|
|
202
|
+
* Detect and escape a stale worktree cwd (#608).
|
|
203
|
+
*
|
|
204
|
+
* After milestone completion + merge, the worktree directory is removed but
|
|
205
|
+
* the process cwd may still point inside `.gsd/worktrees/<MID>/`.
|
|
206
|
+
* When a new session starts, `process.cwd()` is passed as `base` to startAuto
|
|
207
|
+
* and all subsequent writes land in the wrong directory. This function detects
|
|
208
|
+
* that scenario and chdir back to the project root.
|
|
209
|
+
*
|
|
210
|
+
* Returns the corrected base path.
|
|
211
|
+
*/
|
|
212
|
+
function escapeStaleWorktree(base: string): string {
|
|
213
|
+
const marker = `${pathSep}.gsd${pathSep}worktrees${pathSep}`;
|
|
214
|
+
const idx = base.indexOf(marker);
|
|
215
|
+
if (idx === -1) return base;
|
|
216
|
+
|
|
217
|
+
// base is inside .gsd/worktrees/<something> — extract the project root
|
|
218
|
+
const projectRoot = base.slice(0, idx);
|
|
219
|
+
try {
|
|
220
|
+
process.chdir(projectRoot);
|
|
221
|
+
} catch {
|
|
222
|
+
// If chdir fails, return the original — caller will handle errors downstream
|
|
223
|
+
return base;
|
|
224
|
+
}
|
|
225
|
+
return projectRoot;
|
|
226
|
+
}
|
|
227
|
+
|
|
199
228
|
/** Crash recovery prompt — set by startAuto, consumed by first dispatchNextUnit */
|
|
200
229
|
let pendingCrashRecovery: string | null = null;
|
|
201
230
|
|
|
@@ -228,6 +257,9 @@ const DISPATCH_GAP_TIMEOUT_MS = 5_000; // 5 seconds
|
|
|
228
257
|
/** SIGTERM handler registered while auto-mode is active — cleared on stop/pause. */
|
|
229
258
|
let _sigtermHandler: (() => void) | null = null;
|
|
230
259
|
|
|
260
|
+
/** Tool calls currently being executed — prevents false idle detection during long-running tools. */
|
|
261
|
+
const inFlightTools = new Set<string>();
|
|
262
|
+
|
|
231
263
|
type BudgetAlertLevel = 0 | 75 | 90 | 100;
|
|
232
264
|
|
|
233
265
|
export function getBudgetAlertLevel(budgetPct: number): BudgetAlertLevel {
|
|
@@ -293,6 +325,22 @@ export function isAutoPaused(): boolean {
|
|
|
293
325
|
return paused;
|
|
294
326
|
}
|
|
295
327
|
|
|
328
|
+
/**
|
|
329
|
+
* Mark a tool execution as in-flight. Called from index.ts on tool_execution_start.
|
|
330
|
+
* Prevents the idle watchdog from declaring the agent idle while tools are executing.
|
|
331
|
+
*/
|
|
332
|
+
export function markToolStart(toolCallId: string): void {
|
|
333
|
+
if (!active) return;
|
|
334
|
+
inFlightTools.add(toolCallId);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Mark a tool execution as completed. Called from index.ts on tool_execution_end.
|
|
339
|
+
*/
|
|
340
|
+
export function markToolEnd(toolCallId: string): void {
|
|
341
|
+
inFlightTools.delete(toolCallId);
|
|
342
|
+
}
|
|
343
|
+
|
|
296
344
|
/**
|
|
297
345
|
* Return the base path to use for the auto.lock file.
|
|
298
346
|
* Always uses the original project root (not the worktree) so that
|
|
@@ -345,6 +393,7 @@ function clearUnitTimeout(): void {
|
|
|
345
393
|
clearInterval(idleWatchdogHandle);
|
|
346
394
|
idleWatchdogHandle = null;
|
|
347
395
|
}
|
|
396
|
+
inFlightTools.clear();
|
|
348
397
|
clearDispatchGapWatchdog();
|
|
349
398
|
}
|
|
350
399
|
|
|
@@ -426,14 +475,18 @@ export async function stopAuto(ctx?: ExtensionContext, pi?: ExtensionAPI): Promi
|
|
|
426
475
|
`Auto-worktree teardown failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
427
476
|
"warning",
|
|
428
477
|
);
|
|
429
|
-
// Force basePath back to original even if teardown failed
|
|
430
|
-
if (originalBasePath) {
|
|
431
|
-
basePath = originalBasePath;
|
|
432
|
-
try { process.chdir(basePath); } catch { /* best-effort */ }
|
|
433
|
-
}
|
|
434
478
|
}
|
|
435
479
|
}
|
|
436
480
|
|
|
481
|
+
// Always restore cwd to project root on stop (#608).
|
|
482
|
+
// Even if isInAutoWorktree returned false (e.g., module state was already
|
|
483
|
+
// cleared by mergeMilestoneToMain), the process cwd may still be inside
|
|
484
|
+
// the worktree directory. Force it back to originalBasePath.
|
|
485
|
+
if (originalBasePath) {
|
|
486
|
+
basePath = originalBasePath;
|
|
487
|
+
try { process.chdir(basePath); } catch { /* best-effort */ }
|
|
488
|
+
}
|
|
489
|
+
|
|
437
490
|
const ledger = getLedger();
|
|
438
491
|
if (ledger && ledger.units.length > 0) {
|
|
439
492
|
const totals = getProjectTotals(ledger.units);
|
|
@@ -458,12 +511,15 @@ export async function stopAuto(ctx?: ExtensionContext, pi?: ExtensionAPI): Promi
|
|
|
458
511
|
stepMode = false;
|
|
459
512
|
unitDispatchCount.clear();
|
|
460
513
|
unitRecoveryCount.clear();
|
|
514
|
+
inFlightTools.clear();
|
|
461
515
|
lastBudgetAlertLevel = 0;
|
|
462
516
|
unitLifetimeDispatches.clear();
|
|
463
517
|
currentUnit = null;
|
|
464
518
|
currentMilestoneId = null;
|
|
465
519
|
originalBasePath = "";
|
|
520
|
+
completedUnits = [];
|
|
466
521
|
clearSliceProgressCache();
|
|
522
|
+
clearActivityLogState();
|
|
467
523
|
pendingCrashRecovery = null;
|
|
468
524
|
_handlingAgentEnd = false;
|
|
469
525
|
ctx?.ui.setStatus("gsd-auto", undefined);
|
|
@@ -519,6 +575,11 @@ export async function startAuto(
|
|
|
519
575
|
): Promise<void> {
|
|
520
576
|
const requestedStepMode = options?.step ?? false;
|
|
521
577
|
|
|
578
|
+
// Escape stale worktree cwd from a previous milestone (#608).
|
|
579
|
+
// After milestone merge + worktree removal, the process cwd may still point
|
|
580
|
+
// inside .gsd/worktrees/<MID>/ — detect and chdir back to project root.
|
|
581
|
+
base = escapeStaleWorktree(base);
|
|
582
|
+
|
|
522
583
|
// If resuming from paused state, just re-activate and dispatch next unit.
|
|
523
584
|
// The conversation is still intact — no need to reinitialize everything.
|
|
524
585
|
if (paused) {
|
|
@@ -569,17 +630,17 @@ export async function startAuto(
|
|
|
569
630
|
ctx.ui.setFooter(hideFooter);
|
|
570
631
|
ctx.ui.notify(stepMode ? "Step-mode resumed." : "Auto-mode resumed.", "info");
|
|
571
632
|
// Restore hook state from disk in case session was interrupted
|
|
572
|
-
restoreHookState(
|
|
633
|
+
restoreHookState(basePath);
|
|
573
634
|
// Rebuild disk state before resuming — user interaction during pause may have changed files
|
|
574
|
-
try { await rebuildState(
|
|
635
|
+
try { await rebuildState(basePath); } catch { /* non-fatal */ }
|
|
575
636
|
try {
|
|
576
|
-
const report = await runGSDDoctor(
|
|
637
|
+
const report = await runGSDDoctor(basePath, { fix: true });
|
|
577
638
|
if (report.fixesApplied.length > 0) {
|
|
578
639
|
ctx.ui.notify(`Resume: applied ${report.fixesApplied.length} fix(es) to state.`, "info");
|
|
579
640
|
}
|
|
580
641
|
} catch { /* non-fatal */ }
|
|
581
642
|
// Self-heal: clear stale runtime records where artifacts already exist
|
|
582
|
-
await selfHealRuntimeRecords(
|
|
643
|
+
await selfHealRuntimeRecords(basePath, ctx, completedKeySet);
|
|
583
644
|
invalidateAllCaches();
|
|
584
645
|
await dispatchNextUnit(ctx, pi);
|
|
585
646
|
return;
|
|
@@ -1251,6 +1312,11 @@ async function dispatchNextUnit(
|
|
|
1251
1312
|
unitLifetimeDispatches.clear();
|
|
1252
1313
|
// Capture integration branch for the new milestone and update git service
|
|
1253
1314
|
captureIntegrationBranch(originalBasePath || basePath, mid, { commitDocs: loadEffectiveGSDPreferences()?.preferences?.git?.commit_docs });
|
|
1315
|
+
// Prune completed milestone from queue order file
|
|
1316
|
+
const pendingIds = state.registry
|
|
1317
|
+
.filter(m => m.status !== "complete")
|
|
1318
|
+
.map(m => m.id);
|
|
1319
|
+
pruneQueueOrder(basePath, pendingIds);
|
|
1254
1320
|
}
|
|
1255
1321
|
if (mid) {
|
|
1256
1322
|
currentMilestoneId = mid;
|
|
@@ -1331,6 +1397,39 @@ async function dispatchNextUnit(
|
|
|
1331
1397
|
`Milestone merge failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
1332
1398
|
"warning",
|
|
1333
1399
|
);
|
|
1400
|
+
// Ensure cwd is restored even if merge failed partway through (#608).
|
|
1401
|
+
// mergeMilestoneToMain may have chdir'd but then thrown, leaving us
|
|
1402
|
+
// in an indeterminate location.
|
|
1403
|
+
if (originalBasePath) {
|
|
1404
|
+
basePath = originalBasePath;
|
|
1405
|
+
try { process.chdir(basePath); } catch { /* best-effort */ }
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
} else if (currentMilestoneId && !isInAutoWorktree(basePath)) {
|
|
1409
|
+
// Branch isolation mode (#603): no worktree, but we may be on a milestone/* branch.
|
|
1410
|
+
// Squash-merge back to the integration branch (or main) before stopping.
|
|
1411
|
+
try {
|
|
1412
|
+
const currentBranch = getCurrentBranch(basePath);
|
|
1413
|
+
const milestoneBranch = autoWorktreeBranch(currentMilestoneId);
|
|
1414
|
+
if (currentBranch === milestoneBranch) {
|
|
1415
|
+
const roadmapPath = resolveMilestoneFile(basePath, currentMilestoneId, "ROADMAP");
|
|
1416
|
+
if (roadmapPath) {
|
|
1417
|
+
const roadmapContent = readFileSync(roadmapPath, "utf-8");
|
|
1418
|
+
// mergeMilestoneToMain handles: auto-commit, checkout integration branch,
|
|
1419
|
+
// squash merge, commit, optional push, branch deletion.
|
|
1420
|
+
const mergeResult = mergeMilestoneToMain(basePath, currentMilestoneId, roadmapContent);
|
|
1421
|
+
gitService = new GitServiceImpl(basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
|
|
1422
|
+
ctx.ui.notify(
|
|
1423
|
+
`Milestone ${currentMilestoneId} merged (branch mode).${mergeResult.pushed ? " Pushed to remote." : ""}`,
|
|
1424
|
+
"info",
|
|
1425
|
+
);
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
} catch (err) {
|
|
1429
|
+
ctx.ui.notify(
|
|
1430
|
+
`Milestone merge failed (branch mode): ${err instanceof Error ? err.message : String(err)}`,
|
|
1431
|
+
"warning",
|
|
1432
|
+
);
|
|
1334
1433
|
}
|
|
1335
1434
|
}
|
|
1336
1435
|
sendDesktopNotification("GSD", `Milestone ${mid} complete!`, "success", "milestone");
|
|
@@ -1757,6 +1856,10 @@ async function dispatchNextUnit(
|
|
|
1757
1856
|
startedAt: currentUnit.startedAt,
|
|
1758
1857
|
finishedAt: Date.now(),
|
|
1759
1858
|
});
|
|
1859
|
+
// Cap to last 200 entries to prevent unbounded growth (#611)
|
|
1860
|
+
if (completedUnits.length > 200) {
|
|
1861
|
+
completedUnits = completedUnits.slice(-200);
|
|
1862
|
+
}
|
|
1760
1863
|
clearUnitRuntimeRecord(basePath, currentUnit.type, currentUnit.id);
|
|
1761
1864
|
unitDispatchCount.delete(`${currentUnit.type}/${currentUnit.id}`);
|
|
1762
1865
|
unitRecoveryCount.delete(`${currentUnit.type}/${currentUnit.id}`);
|
|
@@ -1957,6 +2060,16 @@ async function dispatchNextUnit(
|
|
|
1957
2060
|
if (!runtime) return;
|
|
1958
2061
|
if (Date.now() - runtime.lastProgressAt < idleTimeoutMs) return;
|
|
1959
2062
|
|
|
2063
|
+
// Agent has tool calls currently executing (await_job, long bash, etc.) —
|
|
2064
|
+
// not idle, just waiting for tool completion.
|
|
2065
|
+
if (inFlightTools.size > 0) {
|
|
2066
|
+
writeUnitRuntimeRecord(basePath, unitType, unitId, currentUnit.startedAt, {
|
|
2067
|
+
lastProgressAt: Date.now(),
|
|
2068
|
+
lastProgressKind: "tool-in-flight",
|
|
2069
|
+
});
|
|
2070
|
+
return;
|
|
2071
|
+
}
|
|
2072
|
+
|
|
1960
2073
|
// Before triggering recovery, check if the agent is actually producing
|
|
1961
2074
|
// work on disk. `git status --porcelain` is cheap and catches any
|
|
1962
2075
|
// staged/unstaged/untracked changes the agent made since lastProgressAt.
|