gsd-lite 0.5.13 → 0.5.14
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/.mcp.json +0 -0
- package/README.md +5 -5
- package/agents/debugger.md +0 -0
- package/agents/executor.md +0 -0
- package/agents/researcher.md +0 -0
- package/agents/reviewer.md +0 -0
- package/commands/doctor.md +0 -0
- package/commands/prd.md +0 -0
- package/commands/resume.md +0 -0
- package/commands/start.md +0 -0
- package/commands/status.md +0 -0
- package/commands/stop.md +0 -0
- package/hooks/context-monitor.js +0 -0
- package/hooks/gsd-auto-update.cjs +0 -0
- package/hooks/gsd-context-monitor.cjs +0 -0
- package/hooks/gsd-session-init.cjs +0 -0
- package/hooks/gsd-session-stop.cjs +0 -0
- package/hooks/gsd-statusline.cjs +1 -18
- package/hooks/hooks.json +0 -0
- package/hooks/lib/gsd-finder.cjs +0 -0
- package/install.js +0 -0
- package/launcher.js +0 -0
- package/package.json +1 -1
- package/references/anti-rationalization-full.md +0 -0
- package/references/evidence-spec.md +0 -0
- package/references/execution-loop.md +0 -0
- package/references/git-worktrees.md +0 -0
- package/references/questioning.md +0 -0
- package/references/review-classification.md +0 -0
- package/references/state-diagram.md +0 -0
- package/references/testing-patterns.md +0 -0
- package/src/schema.js +0 -0
- package/src/server.js +0 -0
- package/src/tools/orchestrator/debugger.js +2 -0
- package/src/tools/orchestrator/executor.js +3 -0
- package/src/tools/orchestrator/helpers.js +2 -0
- package/src/tools/orchestrator/index.js +0 -0
- package/src/tools/orchestrator/researcher.js +3 -5
- package/src/tools/orchestrator/resume.js +0 -0
- package/src/tools/orchestrator/reviewer.js +38 -3
- package/src/tools/state/constants.js +0 -0
- package/src/tools/state/crud.js +13 -2
- package/src/tools/state/index.js +0 -0
- package/src/tools/state/logic.js +9 -1
- package/src/tools/verify.js +0 -0
- package/src/utils.js +12 -1
- package/uninstall.js +0 -0
- package/workflows/debugging.md +0 -0
- package/workflows/deviation-rules.md +0 -0
- package/workflows/execution-flow.md +0 -0
- package/workflows/research.md +0 -0
- package/workflows/review-cycle.md +0 -0
- package/workflows/tdd-cycle.md +0 -0
|
@@ -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.5.
|
|
16
|
+
"version": "0.5.14",
|
|
17
17
|
"keywords": [
|
|
18
18
|
"orchestration",
|
|
19
19
|
"mcp",
|
package/.mcp.json
CHANGED
|
File without changes
|
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ GSD-Lite is an AI orchestration tool for [Claude Code](https://docs.anthropic.co
|
|
|
17
17
|
### Quality Discipline (Built-in, Not Optional)
|
|
18
18
|
- **TDD enforcement** — "No production code without a failing test first" baked into every executor dispatch
|
|
19
19
|
- **Anti-rationalization guards** — Red-flag checklists inline in every agent prompt, blocking common excuses to skip process
|
|
20
|
-
- **Multi-level code review** — L0 self-review / L1 phase-batch review / L2 immediate independent review
|
|
20
|
+
- **Multi-level code review** — L0 self-review / L1 phase-batch review / L2 immediate independent review / phase review retry limit
|
|
21
21
|
- **Contract change propagation** — When an API contract changes, downstream tasks automatically invalidate
|
|
22
22
|
|
|
23
23
|
### Intelligent Failure Recovery
|
|
@@ -27,7 +27,7 @@ GSD-Lite is an AI orchestration tool for [Claude Code](https://docs.anthropic.co
|
|
|
27
27
|
- **Rework propagation** — Critical review issues cascade invalidation to dependent tasks
|
|
28
28
|
|
|
29
29
|
### Adaptive Review & Parallel Execution
|
|
30
|
-
- **Confidence-based review adjustment** — Executor self-assesses confidence (high/medium/low); orchestrator auto-adjusts review level
|
|
30
|
+
- **Confidence-based review adjustment** — Executor self-assesses confidence (high/medium/low); orchestrator auto-adjusts review level with evidence cross-validation
|
|
31
31
|
- **Impact analysis before review** — Reviewer runs impact analysis on multi-file changes to catch missed downstream effects
|
|
32
32
|
- **Parallel task scheduling** — Independent tasks within the same phase are identified for concurrent dispatch
|
|
33
33
|
- **Auto PR suggestion** — Phase/project completion prompts PR creation with evidence summary
|
|
@@ -249,7 +249,7 @@ gsd-lite/
|
|
|
249
249
|
├── references/ # 8 reference docs
|
|
250
250
|
├── hooks/ # Session lifecycle (StatusLine + PostToolUse + SessionStart + Stop + AutoUpdate)
|
|
251
251
|
│ └── lib/ # Shared hook utilities (gsd-finder)
|
|
252
|
-
├── tests/ #
|
|
252
|
+
├── tests/ # 826 tests (unit + simulation + E2E)
|
|
253
253
|
├── cli.js # Install/uninstall CLI entry
|
|
254
254
|
├── install.js # Installation script
|
|
255
255
|
└── uninstall.js # Uninstall script
|
|
@@ -258,8 +258,8 @@ gsd-lite/
|
|
|
258
258
|
## Testing
|
|
259
259
|
|
|
260
260
|
```bash
|
|
261
|
-
npm test # Run all
|
|
262
|
-
npm run test:coverage # Tests + coverage report (94%+ lines,
|
|
261
|
+
npm test # Run all 826 tests
|
|
262
|
+
npm run test:coverage # Tests + coverage report (94%+ lines, 83%+ branches)
|
|
263
263
|
npm run lint # Biome lint
|
|
264
264
|
node --test tests/file.js # Run a single test file
|
|
265
265
|
```
|
package/agents/debugger.md
CHANGED
|
File without changes
|
package/agents/executor.md
CHANGED
|
File without changes
|
package/agents/researcher.md
CHANGED
|
File without changes
|
package/agents/reviewer.md
CHANGED
|
File without changes
|
package/commands/doctor.md
CHANGED
|
File without changes
|
package/commands/prd.md
CHANGED
|
File without changes
|
package/commands/resume.md
CHANGED
|
File without changes
|
package/commands/start.md
CHANGED
|
File without changes
|
package/commands/status.md
CHANGED
|
File without changes
|
package/commands/stop.md
CHANGED
|
File without changes
|
package/hooks/context-monitor.js
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/hooks/gsd-statusline.cjs
CHANGED
|
@@ -6,24 +6,7 @@
|
|
|
6
6
|
const fs = require('node:fs');
|
|
7
7
|
const path = require('node:path');
|
|
8
8
|
const os = require('node:os');
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Walk from startDir up to filesystem root looking for a .gsd directory.
|
|
12
|
-
* Returns the absolute path to .gsd if found, or null.
|
|
13
|
-
*/
|
|
14
|
-
function findGsdDir(startDir) {
|
|
15
|
-
let dir = startDir;
|
|
16
|
-
while (true) {
|
|
17
|
-
const candidate = path.join(dir, '.gsd');
|
|
18
|
-
try {
|
|
19
|
-
if (fs.statSync(candidate).isDirectory()) return candidate;
|
|
20
|
-
} catch {
|
|
21
|
-
const parent = path.dirname(dir);
|
|
22
|
-
if (parent === dir) return null; // reached filesystem root
|
|
23
|
-
dir = parent;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
}
|
|
9
|
+
const { findGsdDir } = require('./lib/gsd-finder.cjs');
|
|
27
10
|
|
|
28
11
|
let input = '';
|
|
29
12
|
const stdinTimeout = setTimeout(() => process.exit(0), 3000);
|
package/hooks/hooks.json
CHANGED
|
File without changes
|
package/hooks/lib/gsd-finder.cjs
CHANGED
|
File without changes
|
package/install.js
CHANGED
|
File without changes
|
package/launcher.js
CHANGED
|
File without changes
|
package/package.json
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/src/schema.js
CHANGED
|
File without changes
|
package/src/server.js
CHANGED
|
File without changes
|
|
@@ -16,6 +16,8 @@ export async function handleDebuggerResult({ result, basePath = process.cwd() }
|
|
|
16
16
|
return { error: true, message: `Invalid debugger result: ${validation.errors.join('; ')}` };
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
// Note: read() is outside the state lock — safe under single-session sequential execution.
|
|
20
|
+
// See executor.js for rationale.
|
|
19
21
|
const state = await read({ basePath });
|
|
20
22
|
if (state.error) return state;
|
|
21
23
|
const { phase, task } = getPhaseAndTask(state, result.task_id);
|
|
@@ -20,6 +20,9 @@ export async function handleExecutorResult({ result, basePath = process.cwd() }
|
|
|
20
20
|
return { error: true, message: `Invalid executor result: ${validation.errors.join('; ')}` };
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
// Note: read() is outside the state lock. This is safe because the MCP server
|
|
24
|
+
// processes tool calls sequentially (single-session, promise-queue serialized).
|
|
25
|
+
// persist() below re-acquires the lock and applies changes atomically.
|
|
23
26
|
const state = await read({ basePath });
|
|
24
27
|
if (state.error) return state;
|
|
25
28
|
const { phase, task } = getPhaseAndTask(state, result.task_id);
|
|
@@ -10,6 +10,7 @@ import { getGitHead, getGsdDir } from '../../utils.js';
|
|
|
10
10
|
const MAX_DEBUG_RETRY = 3;
|
|
11
11
|
const MAX_RESUME_DEPTH = 3;
|
|
12
12
|
const CONTEXT_RESUME_THRESHOLD = 40;
|
|
13
|
+
const MAX_PHASE_REVIEW_RETRY = 5;
|
|
13
14
|
|
|
14
15
|
// ── Result Contracts ──
|
|
15
16
|
// Provided in dispatch responses so agents produce valid results on the first call.
|
|
@@ -424,6 +425,7 @@ export {
|
|
|
424
425
|
MAX_DEBUG_RETRY,
|
|
425
426
|
MAX_RESUME_DEPTH,
|
|
426
427
|
CONTEXT_RESUME_THRESHOLD,
|
|
428
|
+
MAX_PHASE_REVIEW_RETRY,
|
|
427
429
|
RESULT_CONTRACTS,
|
|
428
430
|
isTerminalWorkflowMode,
|
|
429
431
|
parseTimestamp,
|
|
File without changes
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { storeResearch } from '../state/index.js';
|
|
2
2
|
import { validateResearcherResult } from '../../schema.js';
|
|
3
|
-
import { resumeWorkflow } from './resume.js';
|
|
4
3
|
|
|
5
4
|
export async function handleResearcherResult({ result, artifacts, decision_index, basePath = process.cwd() } = {}) {
|
|
6
5
|
if (!result || typeof result !== 'object' || Array.isArray(result)) {
|
|
@@ -15,11 +14,10 @@ export async function handleResearcherResult({ result, artifacts, decision_index
|
|
|
15
14
|
const persisted = await storeResearch({ result, artifacts, decision_index, basePath });
|
|
16
15
|
if (persisted.error) return persisted;
|
|
17
16
|
|
|
18
|
-
const resumed = await resumeWorkflow({ basePath });
|
|
19
|
-
if (resumed.error) return resumed;
|
|
20
|
-
|
|
21
17
|
return {
|
|
22
|
-
|
|
18
|
+
success: true,
|
|
19
|
+
action: 'research_stored',
|
|
20
|
+
workflow_mode: persisted.workflow_mode,
|
|
23
21
|
stored_files: persisted.stored_files,
|
|
24
22
|
decision_ids: persisted.decision_ids,
|
|
25
23
|
research_warnings: persisted.warnings,
|
|
File without changes
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { read } from '../state/index.js';
|
|
2
2
|
import { validateReviewerResult } from '../../schema.js';
|
|
3
3
|
import {
|
|
4
|
+
MAX_PHASE_REVIEW_RETRY,
|
|
4
5
|
getCurrentPhase,
|
|
5
6
|
getTaskById,
|
|
6
7
|
persist,
|
|
@@ -15,6 +16,8 @@ export async function handleReviewerResult({ result, basePath = process.cwd() }
|
|
|
15
16
|
return { error: true, message: `Invalid reviewer result: ${validation.errors.join('; ')}` };
|
|
16
17
|
}
|
|
17
18
|
|
|
19
|
+
// Note: read() is outside the state lock — safe under single-session sequential execution.
|
|
20
|
+
// See executor.js for rationale.
|
|
18
21
|
const state = await read({ basePath });
|
|
19
22
|
if (state.error) return state;
|
|
20
23
|
|
|
@@ -70,6 +73,40 @@ export async function handleReviewerResult({ result, basePath = process.cwd() }
|
|
|
70
73
|
const specFailed = result.spec_passed === false;
|
|
71
74
|
const qualityFailed = result.quality_passed === false;
|
|
72
75
|
const needsRework = hasCritical || specFailed || qualityFailed;
|
|
76
|
+
|
|
77
|
+
// Compute retry count once for both exhaustion check and state update
|
|
78
|
+
const currentRetryCount = phase.phase_review?.retry_count || 0;
|
|
79
|
+
const nextRetryCount = needsRework ? currentRetryCount + 1 : 0;
|
|
80
|
+
|
|
81
|
+
// Phase review retry limit: prevent infinite reviewing↔active cycles
|
|
82
|
+
if (needsRework && nextRetryCount > MAX_PHASE_REVIEW_RETRY) {
|
|
83
|
+
const persistError = await persist(basePath, {
|
|
84
|
+
workflow_mode: 'awaiting_user',
|
|
85
|
+
current_task: null,
|
|
86
|
+
current_review: {
|
|
87
|
+
scope: 'phase',
|
|
88
|
+
scope_id: phase.id,
|
|
89
|
+
stage: 'review_retry_exhausted',
|
|
90
|
+
retry_count: nextRetryCount,
|
|
91
|
+
},
|
|
92
|
+
phases: [{
|
|
93
|
+
id: phase.id,
|
|
94
|
+
lifecycle: phase.lifecycle === 'reviewing' ? 'active' : phase.lifecycle,
|
|
95
|
+
phase_review: { status: 'rework_required', retry_count: nextRetryCount },
|
|
96
|
+
}],
|
|
97
|
+
});
|
|
98
|
+
if (persistError) return persistError;
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
success: true,
|
|
102
|
+
action: 'review_retry_exhausted',
|
|
103
|
+
workflow_mode: 'awaiting_user',
|
|
104
|
+
phase_id: phase.id,
|
|
105
|
+
retry_count: nextRetryCount,
|
|
106
|
+
message: `Phase ${phase.id} review failed ${nextRetryCount} times (limit: ${MAX_PHASE_REVIEW_RETRY}). User intervention required.`,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
73
110
|
const reviewStatus = needsRework ? 'rework_required' : 'accepted';
|
|
74
111
|
|
|
75
112
|
// done is auto-recomputed by update() — no manual tracking needed
|
|
@@ -77,9 +114,7 @@ export async function handleReviewerResult({ result, basePath = process.cwd() }
|
|
|
77
114
|
id: phase.id,
|
|
78
115
|
phase_review: {
|
|
79
116
|
status: reviewStatus,
|
|
80
|
-
|
|
81
|
-
? { retry_count: (phase.phase_review?.retry_count || 0) + 1 }
|
|
82
|
-
: { retry_count: 0 }),
|
|
117
|
+
retry_count: nextRetryCount,
|
|
83
118
|
},
|
|
84
119
|
todo: taskPatches,
|
|
85
120
|
};
|
|
File without changes
|
package/src/tools/state/crud.js
CHANGED
|
@@ -12,7 +12,6 @@ import {
|
|
|
12
12
|
createInitialState,
|
|
13
13
|
migrateState,
|
|
14
14
|
} from '../../schema.js';
|
|
15
|
-
import { runAll } from '../verify.js';
|
|
16
15
|
import {
|
|
17
16
|
ERROR_CODES,
|
|
18
17
|
MAX_EVIDENCE_ENTRIES,
|
|
@@ -29,6 +28,11 @@ export async function init({ project, phases, research, force = false, basePath
|
|
|
29
28
|
if (!project || typeof project !== 'string') {
|
|
30
29
|
return { error: true, code: ERROR_CODES.INVALID_INPUT, message: 'project must be a non-empty string' };
|
|
31
30
|
}
|
|
31
|
+
// Sanitize: strip HTML comment delimiters (could break marker-based CLAUDE.md injection) and cap length
|
|
32
|
+
project = project.replace(/<!--|-->/g, '').trim().slice(0, 200);
|
|
33
|
+
if (!project) {
|
|
34
|
+
return { error: true, code: ERROR_CODES.INVALID_INPUT, message: 'project name is empty after sanitization' };
|
|
35
|
+
}
|
|
32
36
|
if (!Array.isArray(phases)) {
|
|
33
37
|
return { error: true, code: ERROR_CODES.INVALID_INPUT, message: 'phases must be an array' };
|
|
34
38
|
}
|
|
@@ -420,7 +424,14 @@ export async function phaseComplete({
|
|
|
420
424
|
};
|
|
421
425
|
}
|
|
422
426
|
|
|
423
|
-
|
|
427
|
+
if (run_verify && !verification) {
|
|
428
|
+
return {
|
|
429
|
+
error: true,
|
|
430
|
+
code: ERROR_CODES.INVALID_INPUT,
|
|
431
|
+
message: 'run_verify requires verification results to be passed via the verification parameter; the state layer does not execute external tools',
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
const verificationResult = verification || null;
|
|
424
435
|
const testsPassed = verificationResult
|
|
425
436
|
? verificationPassed(verificationResult)
|
|
426
437
|
: phase.phase_handoff.tests_passed === true;
|
package/src/tools/state/index.js
CHANGED
|
File without changes
|
package/src/tools/state/logic.js
CHANGED
|
@@ -269,9 +269,17 @@ export function reclassifyReviewLevel(task, executorResult) {
|
|
|
269
269
|
}
|
|
270
270
|
|
|
271
271
|
// High confidence on non-sensitive L1 tasks → downgrade to L0 (self-review sufficient)
|
|
272
|
+
// Cross-validate: require objective evidence before trusting self-reported confidence.
|
|
273
|
+
// Without evidence or with failed tests, confidence claim is not credible.
|
|
272
274
|
if (executorResult.confidence === 'high' && currentLevel === 'L1'
|
|
273
275
|
&& !executorResult.contract_changed) {
|
|
274
|
-
|
|
276
|
+
const hasEvidence = Array.isArray(executorResult.evidence) && executorResult.evidence.length > 0;
|
|
277
|
+
const hasTestFailure = Array.isArray(executorResult.evidence)
|
|
278
|
+
&& executorResult.evidence.some(e => e && e.type === 'test' && e.passed === false);
|
|
279
|
+
if (hasEvidence && !hasTestFailure) {
|
|
280
|
+
return 'L0';
|
|
281
|
+
}
|
|
282
|
+
// Insufficient evidence or test failure — stay at L1 despite high confidence claim
|
|
275
283
|
}
|
|
276
284
|
|
|
277
285
|
return currentLevel;
|
package/src/tools/verify.js
CHANGED
|
File without changes
|
package/src/utils.js
CHANGED
|
@@ -65,6 +65,7 @@ const LOCK_MAX_RETRIES = 100; // 5 seconds total
|
|
|
65
65
|
*/
|
|
66
66
|
export async function withFileLock(lockPath, fn) {
|
|
67
67
|
let acquired = false;
|
|
68
|
+
let nonLockError = false;
|
|
68
69
|
for (let i = 0; i < LOCK_MAX_RETRIES; i++) {
|
|
69
70
|
try {
|
|
70
71
|
await writeFile(lockPath, String(process.pid), { flag: 'wx' });
|
|
@@ -84,11 +85,21 @@ export async function withFileLock(lockPath, fn) {
|
|
|
84
85
|
}
|
|
85
86
|
await new Promise(r => setTimeout(r, LOCK_RETRY_MS));
|
|
86
87
|
} else {
|
|
87
|
-
|
|
88
|
+
// Non-EEXIST error (e.g., read-only fs) — proceed without lock
|
|
89
|
+
nonLockError = true;
|
|
90
|
+
break;
|
|
88
91
|
}
|
|
89
92
|
}
|
|
90
93
|
}
|
|
91
94
|
|
|
95
|
+
// Lock exhaustion (retries depleted while another process held the lock):
|
|
96
|
+
// throw to prevent concurrent unlocked writes that cause data corruption.
|
|
97
|
+
// Non-EEXIST errors (read-only fs, permission denied) still proceed without lock
|
|
98
|
+
// since locking is physically impossible in those environments.
|
|
99
|
+
if (!acquired && !nonLockError) {
|
|
100
|
+
throw new Error(`Lock acquisition timeout: could not acquire ${lockPath} after ${LOCK_MAX_RETRIES} retries (${LOCK_MAX_RETRIES * LOCK_RETRY_MS}ms)`);
|
|
101
|
+
}
|
|
102
|
+
|
|
92
103
|
try {
|
|
93
104
|
return await fn();
|
|
94
105
|
} finally {
|
package/uninstall.js
CHANGED
|
File without changes
|
package/workflows/debugging.md
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/workflows/research.md
CHANGED
|
File without changes
|
|
File without changes
|
package/workflows/tdd-cycle.md
CHANGED
|
File without changes
|