gsd-lite 0.7.4 → 0.7.6
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +2 -2
- package/hooks/lib/statusline-composite.cjs +55 -40
- package/package.json +1 -1
- package/src/server.js +1 -1
- package/src/tools/orchestrator/executor.js +16 -4
- package/src/tools/state/crud.js +7 -0
- package/workflows/execution-flow.md +4 -9
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"name": "gsd",
|
|
14
14
|
"source": "./",
|
|
15
15
|
"description": "AI orchestration tool — GSD management shell + Superpowers quality core. 5 commands, 4 agents, 5 workflows, MCP server, context monitoring.",
|
|
16
|
-
"version": "0.7.
|
|
16
|
+
"version": "0.7.6",
|
|
17
17
|
"keywords": [
|
|
18
18
|
"orchestration",
|
|
19
19
|
"mcp",
|
package/README.md
CHANGED
|
@@ -291,7 +291,7 @@ gsd-lite/
|
|
|
291
291
|
│ ├── gsd-session-stop.cjs # Graceful shutdown with crash markers
|
|
292
292
|
│ ├── gsd-statusline.cjs # StatusLine display (composite-aware)
|
|
293
293
|
│ └── lib/ # Shared hook utilities (gsd-finder, composite statusline, semver)
|
|
294
|
-
├── tests/ #
|
|
294
|
+
├── tests/ # 972 tests (unit + simulation + E2E integration)
|
|
295
295
|
├── cli.js # Install/uninstall CLI entry
|
|
296
296
|
├── install.js # Installation script (plugin-aware, idempotent)
|
|
297
297
|
└── uninstall.js # Uninstall script
|
|
@@ -300,7 +300,7 @@ gsd-lite/
|
|
|
300
300
|
## Testing
|
|
301
301
|
|
|
302
302
|
```bash
|
|
303
|
-
npm test # Run all
|
|
303
|
+
npm test # Run all 972 tests
|
|
304
304
|
npm run test:coverage # Tests + coverage report (94%+ lines, 83%+ branches)
|
|
305
305
|
npm run lint # Biome lint
|
|
306
306
|
node --test tests/file.js # Run a single test file
|
|
@@ -63,33 +63,17 @@ function runChainCLI(args) {
|
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
/**
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
66
|
+
* Rewrite a single registry file to its desired state: exactly one canonical
|
|
67
|
+
* `{id: 'gsd', ...}` entry with the given command, dropping every other entry
|
|
68
|
+
* whose command references gsd-statusline (e.g. ghost `_previous` entries
|
|
69
|
+
* left by code-graph's composite-takeover). Canonical entry is placed before
|
|
70
|
+
* `code-graph` for display priority.
|
|
71
|
+
*
|
|
72
|
+
* Returns true if the registry is now in the desired state (including
|
|
73
|
+
* idempotent no-op), false if the file can't be parsed as an array.
|
|
72
74
|
*/
|
|
73
|
-
function
|
|
74
|
-
const
|
|
75
|
-
if (runChainCLI(['register', 'gsd', command, '--stdin'])) return true;
|
|
76
|
-
|
|
77
|
-
let registryPath = findCompositeRegistry();
|
|
78
|
-
|
|
79
|
-
// If composite statusLine is configured but registry file is missing,
|
|
80
|
-
// create it if the parent directory exists (e.g., code-graph installed
|
|
81
|
-
// but registry was deleted or not yet created).
|
|
82
|
-
if (!registryPath) {
|
|
83
|
-
for (const candidate of REGISTRY_PATHS) {
|
|
84
|
-
const dir = path.dirname(candidate);
|
|
85
|
-
if (fs.existsSync(dir)) {
|
|
86
|
-
registryPath = candidate;
|
|
87
|
-
break;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
if (!registryPath) return false;
|
|
91
|
-
}
|
|
92
|
-
|
|
75
|
+
function normalizeRegistryFile(registryPath, canonicalCommand) {
|
|
76
|
+
const canonical = { id: 'gsd', command: canonicalCommand, needsStdin: true };
|
|
93
77
|
try {
|
|
94
78
|
let registry;
|
|
95
79
|
try {
|
|
@@ -99,24 +83,24 @@ function registerProvider(statuslineScriptPath) {
|
|
|
99
83
|
}
|
|
100
84
|
if (!Array.isArray(registry)) return false;
|
|
101
85
|
|
|
102
|
-
|
|
86
|
+
// Drop every entry pointing at gsd-statusline, regardless of id. This
|
|
87
|
+
// catches the canonical `gsd` slot AND ghosts like `_previous` that
|
|
88
|
+
// code-graph's id-scoped chain CLI `register` can't see.
|
|
89
|
+
const nonGsd = registry.filter(
|
|
90
|
+
e => !(e.command || '').includes('gsd-statusline'),
|
|
91
|
+
);
|
|
103
92
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
93
|
+
const cgIdx = nonGsd.findIndex(e => e.id === 'code-graph');
|
|
94
|
+
if (cgIdx >= 0) nonGsd.splice(cgIdx, 0, canonical);
|
|
95
|
+
else nonGsd.unshift(canonical);
|
|
107
96
|
|
|
108
|
-
if (
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
const cgIdx = registry.findIndex(p => p.id === 'code-graph');
|
|
113
|
-
if (cgIdx >= 0) registry.splice(cgIdx, 0, provider);
|
|
114
|
-
else registry.unshift(provider);
|
|
115
|
-
}
|
|
97
|
+
// Skip write if already in desired state (idempotent re-install).
|
|
98
|
+
const before = JSON.stringify(registry);
|
|
99
|
+
const after = JSON.stringify(nonGsd);
|
|
100
|
+
if (before === after) return true;
|
|
116
101
|
|
|
117
|
-
// Atomic write
|
|
118
102
|
const tmp = registryPath + `.${process.pid}-${Date.now()}.tmp`;
|
|
119
|
-
fs.writeFileSync(tmp, JSON.stringify(
|
|
103
|
+
fs.writeFileSync(tmp, JSON.stringify(nonGsd, null, 2) + '\n');
|
|
120
104
|
fs.renameSync(tmp, registryPath);
|
|
121
105
|
return true;
|
|
122
106
|
} catch {
|
|
@@ -124,6 +108,37 @@ function registerProvider(statuslineScriptPath) {
|
|
|
124
108
|
}
|
|
125
109
|
}
|
|
126
110
|
|
|
111
|
+
/**
|
|
112
|
+
* Register GSD as a provider in the composite statusline registry.
|
|
113
|
+
*
|
|
114
|
+
* Calls code-graph's statusline-chain.js CLI when available, THEN post-scrubs
|
|
115
|
+
* every known registry path. The CLI's `register gsd <cmd>` is id-scoped and
|
|
116
|
+
* silently leaves ghost entries (e.g. `_previous` whose command is our
|
|
117
|
+
* gsd-statusline but whose id isn't `gsd`) in place — that caused
|
|
118
|
+
* double-rendering after upgrades where code-graph had previously promoted a
|
|
119
|
+
* top-level GSD statusLine to `_previous`. Post-normalization guarantees
|
|
120
|
+
* exactly one canonical `gsd` entry per registry regardless of which path
|
|
121
|
+
* succeeded.
|
|
122
|
+
*
|
|
123
|
+
* @param {string} statuslineScriptPath - Absolute path to gsd-statusline.cjs
|
|
124
|
+
* @returns {boolean} true if registered or normalized in at least one registry
|
|
125
|
+
*/
|
|
126
|
+
function registerProvider(statuslineScriptPath) {
|
|
127
|
+
const command = `node ${JSON.stringify(statuslineScriptPath)}`;
|
|
128
|
+
const cliOk = runChainCLI(['register', 'gsd', command, '--stdin']);
|
|
129
|
+
|
|
130
|
+
let anyNormalized = false;
|
|
131
|
+
for (const candidate of REGISTRY_PATHS) {
|
|
132
|
+
// Only touch paths whose parent dir exists — don't create arbitrary
|
|
133
|
+
// ~/.cache/ subtrees on machines without code-graph.
|
|
134
|
+
const dir = path.dirname(candidate);
|
|
135
|
+
if (!fs.existsSync(dir)) continue;
|
|
136
|
+
if (normalizeRegistryFile(candidate, command)) anyNormalized = true;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return cliOk || anyNormalized;
|
|
140
|
+
}
|
|
141
|
+
|
|
127
142
|
/**
|
|
128
143
|
* Remove GSD entry from composite statusline registry.
|
|
129
144
|
* Prefers code-graph's statusline-chain.js CLI when available; falls back to
|
package/package.json
CHANGED
package/src/server.js
CHANGED
|
@@ -144,7 +144,7 @@ const TOOLS = [
|
|
|
144
144
|
},
|
|
145
145
|
run_verify: {
|
|
146
146
|
type: 'boolean',
|
|
147
|
-
description: '
|
|
147
|
+
description: 'Assert that verification was run externally; if true without a verification object, returns INVALID_INPUT. The state layer does not execute external tools.',
|
|
148
148
|
},
|
|
149
149
|
direction_ok: {
|
|
150
150
|
type: 'boolean',
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { read, reclassifyReviewLevel } from '../state/index.js';
|
|
1
|
+
import { read, reclassifyReviewLevel, selectRunnableTask } from '../state/index.js';
|
|
2
2
|
import { validateExecutorResult } from '../../schema.js';
|
|
3
3
|
import {
|
|
4
4
|
MAX_DEBUG_RETRY,
|
|
@@ -101,8 +101,20 @@ export async function handleExecutorResult({ result, basePath = process.cwd() }
|
|
|
101
101
|
|
|
102
102
|
if (result.outcome === 'blocked') {
|
|
103
103
|
const { blocked_reason, unblock_condition } = getBlockedReasonFromResult(result);
|
|
104
|
+
// Probe whether other tasks remain runnable after this one is blocked.
|
|
105
|
+
// Design (docs/gsd-lite-design.md §1399): awaiting_user fires only when
|
|
106
|
+
// 0 runnable tasks remain — blocked-with-others-runnable continues execution.
|
|
107
|
+
const probePhase = {
|
|
108
|
+
...phase,
|
|
109
|
+
todo: phase.todo.map((t) => (t.id === task.id
|
|
110
|
+
? { ...t, lifecycle: 'blocked', blocked_reason, unblock_condition }
|
|
111
|
+
: t)),
|
|
112
|
+
};
|
|
113
|
+
const probe = selectRunnableTask(probePhase, state);
|
|
114
|
+
const hasOtherRunnable = !!probe?.task;
|
|
115
|
+
|
|
104
116
|
const persistError = await persist(basePath, {
|
|
105
|
-
workflow_mode: 'awaiting_user',
|
|
117
|
+
workflow_mode: hasOtherRunnable ? 'executing_task' : 'awaiting_user',
|
|
106
118
|
current_task: null,
|
|
107
119
|
current_review: null,
|
|
108
120
|
phases: [{
|
|
@@ -120,8 +132,8 @@ export async function handleExecutorResult({ result, basePath = process.cwd() }
|
|
|
120
132
|
|
|
121
133
|
return {
|
|
122
134
|
success: true,
|
|
123
|
-
action: 'awaiting_user',
|
|
124
|
-
workflow_mode: 'awaiting_user',
|
|
135
|
+
action: hasOtherRunnable ? 'continue_execution' : 'awaiting_user',
|
|
136
|
+
workflow_mode: hasOtherRunnable ? 'executing_task' : 'awaiting_user',
|
|
125
137
|
task_id: task.id,
|
|
126
138
|
blockers: getBlockedTasks({ todo: [{ id: task.id, lifecycle: 'blocked', blocked_reason, unblock_condition }] }),
|
|
127
139
|
};
|
package/src/tools/state/crud.js
CHANGED
|
@@ -511,6 +511,13 @@ export async function phaseComplete({
|
|
|
511
511
|
? verificationPassed(verificationResult)
|
|
512
512
|
: phase.phase_handoff.tests_passed === true;
|
|
513
513
|
if (!testsPassed) {
|
|
514
|
+
if (!verificationResult) {
|
|
515
|
+
return {
|
|
516
|
+
error: true,
|
|
517
|
+
code: ERROR_CODES.HANDOFF_GATE,
|
|
518
|
+
message: 'Handoff gate not met: verification required. Run lint/typecheck/test externally, then call phase-complete with verification: { lint: { exit_code }, typecheck: { exit_code }, test: { exit_code } }',
|
|
519
|
+
};
|
|
520
|
+
}
|
|
514
521
|
return {
|
|
515
522
|
error: true,
|
|
516
523
|
code: ERROR_CODES.HANDOFF_GATE,
|
|
@@ -163,21 +163,16 @@
|
|
|
163
163
|
| `await_recovery_decision` | 工作流处于 failed 状态。向用户展示失败信息和恢复选项 (retry/skip/replan) |
|
|
164
164
|
|
|
165
165
|
**`phase-complete` 参数:**
|
|
166
|
+
|
|
167
|
+
编排器必须先用 Bash 外部执行 lint/typecheck/test (state 层不自动跑外部工具),然后把结果作为 `verification` 传入:
|
|
166
168
|
```
|
|
167
169
|
phase-complete({
|
|
168
170
|
phase_id: <当前 phase 编号>,
|
|
169
|
-
run_verify: true, // 自动运行 lint/typecheck/test
|
|
170
|
-
direction_ok: true // 方向校验通过 (如有偏差设为 false)
|
|
171
|
-
})
|
|
172
|
-
```
|
|
173
|
-
如果没有 lint/typecheck/test 工具,可改用 `verification` 参数传入预计算结果:
|
|
174
|
-
```
|
|
175
|
-
phase-complete({
|
|
176
|
-
phase_id: <phase>,
|
|
177
171
|
verification: { lint: {exit_code: 0}, typecheck: {exit_code: 0}, test: {exit_code: 0} },
|
|
178
|
-
direction_ok: true
|
|
172
|
+
direction_ok: true // 方向校验通过 (如有偏差设为 false)
|
|
179
173
|
})
|
|
180
174
|
```
|
|
175
|
+
`run_verify: true` 只是声明"已外部跑过",不提供 `verification` 时会返回 INVALID_INPUT,用于防止调用方忘记传结果。如果项目缺少某项脚本 (例如无 typecheck),跑对应工具时返回 0 视为通过即可 (约定: "命令不存在" = 不适用 = 通过)。
|
|
181
176
|
</execution_loop>
|
|
182
177
|
|
|
183
178
|
## STEP 12 — 最终报告
|