@planu/cli 3.9.14 → 4.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/CHANGELOG.md +17 -1
- package/dist/cli/commands/spec.js +20 -1
- package/dist/cli/commands/status.js +18 -1
- package/dist/config/license-plans.json +1 -0
- package/dist/engine/ai-integration/agents-md/generator.js +4 -1
- package/dist/engine/ai-integration/cline/clinerules-generator.js +7 -2
- package/dist/engine/ai-integration/codex/agents-md-generator.js +2 -0
- package/dist/engine/ai-integration/codex/hooks-generator.js +1 -0
- package/dist/engine/ai-integration/cursor/cursorrules-generator.js +7 -2
- package/dist/engine/ai-integration/gemini/settings-generator.js +4 -1
- package/dist/engine/ai-integration/kiro/hooks-generator.js +2 -1
- package/dist/engine/ai-integration/windsurf/windsurfrules-generator.js +7 -2
- package/dist/engine/autopilot/action-registry.js +5 -14
- package/dist/engine/autopilot/state-updater.js +13 -10
- package/dist/engine/cascade-hooks/hooks/git-auto-stage.hook.js +3 -0
- package/dist/engine/cascade-hooks/hooks/html-regen.hook.js +1 -1
- package/dist/engine/cascade-hooks/hooks/status-json.hook.js +1 -1
- package/dist/engine/cascade-hooks/state-drift-detector.d.ts +1 -1
- package/dist/engine/cascade-hooks/state-drift-detector.js +15 -12
- package/dist/engine/git/planu-autocommit.d.ts +1 -0
- package/dist/engine/git/planu-autocommit.js +6 -0
- package/dist/engine/git-hook-injector.js +3 -3
- package/dist/engine/handoff-artifacts/io.js +3 -2
- package/dist/engine/handoff-packager.js +2 -1
- package/dist/engine/hooks/full-spectrum-generator.d.ts +2 -1
- package/dist/engine/hooks/full-spectrum-generator.js +5 -3
- package/dist/engine/marketplace-fetcher/anthropic-source.js +2 -0
- package/dist/engine/opencode/config-scaffold.js +4 -0
- package/dist/engine/release/postmortem-generator.d.ts +1 -1
- package/dist/engine/release/postmortem-generator.js +3 -2
- package/dist/engine/rules-generator/index.js +2 -0
- package/dist/engine/rules-reconciler.js +2 -0
- package/dist/engine/safety/cross-process-lock.js +2 -2
- package/dist/engine/session/checkpoint-writer.js +0 -1
- package/dist/engine/session-context-generator.js +4 -1
- package/dist/engine/skill-bootstrap/skill-writer.js +2 -0
- package/dist/engine/skill-generation/multi-agent-writer.js +2 -0
- package/dist/engine/spec-audit/index.js +2 -2
- package/dist/engine/spec-audit/report-writer.d.ts +1 -1
- package/dist/engine/spec-audit/report-writer.js +5 -4
- package/dist/engine/spec-language/english-only.d.ts +8 -7
- package/dist/engine/spec-language/english-only.js +27 -3
- package/dist/engine/spec-migrator/index.d.ts +1 -0
- package/dist/engine/spec-migrator/index.js +1 -0
- package/dist/engine/spec-migrator/planu-canonical-policy.d.ts +9 -0
- package/dist/engine/spec-migrator/planu-canonical-policy.js +62 -0
- package/dist/engine/spec-migrator/planu-root-cleaner.js +18 -94
- package/dist/engine/spec-migrator/strict-planu-cleanup.d.ts +6 -0
- package/dist/engine/spec-migrator/strict-planu-cleanup.js +199 -0
- package/dist/engine/spec-summary-html.d.ts +5 -5
- package/dist/engine/spec-summary-html.js +7 -32
- package/dist/engine/universal-rules/host-writer.js +8 -2
- package/dist/engine/universal-rules/rules/planu-english-specs.js +9 -5
- package/dist/hosts/claude-code/ux/skills-writer.js +2 -0
- package/dist/hosts/codex/config-scaffold.js +5 -0
- package/dist/hosts/gemini/config-scaffold.js +4 -0
- package/dist/storage/gaps-log.js +4 -4
- package/dist/storage/transition-log.js +3 -2
- package/dist/tools/audit-specs-drift.js +3 -3
- package/dist/tools/create-skill.js +21 -0
- package/dist/tools/create-spec/post-creation.d.ts +2 -1
- package/dist/tools/create-spec/post-creation.js +9 -11
- package/dist/tools/create-spec/spec-builder.js +1 -1
- package/dist/tools/create-spec.js +42 -18
- package/dist/tools/flag-spec-gap.d.ts +1 -1
- package/dist/tools/flag-spec-gap.js +1 -1
- package/dist/tools/generate-dashboard.js +3 -3
- package/dist/tools/housekeeping-sweep.js +16 -0
- package/dist/tools/init-project/agents-md-writer.js +2 -0
- package/dist/tools/init-project/conventions-writer.js +2 -0
- package/dist/tools/init-project/find-skills-writer.js +2 -0
- package/dist/tools/init-project/git-setup.js +11 -2
- package/dist/tools/init-project/handler.js +1 -27
- package/dist/tools/init-project/helpers.js +5 -0
- package/dist/tools/init-project/migration-runner.js +8 -0
- package/dist/tools/init-project/per-client-files-writer.js +2 -0
- package/dist/tools/init-project/planu-workflow-generator.js +2 -0
- package/dist/tools/init-project/rules-generator.js +7 -1
- package/dist/tools/init-project/rules-writer.js +3 -0
- package/dist/tools/init-project/skills-multi-teammate-review-writer.js +2 -0
- package/dist/tools/init-project/skills-writer.js +2 -0
- package/dist/tools/license-gate.d.ts +1 -0
- package/dist/tools/license-gate.js +5 -1
- package/dist/tools/list-specs.js +13 -0
- package/dist/tools/register-sdd-tools.d.ts +1 -1
- package/dist/tools/register-sdd-tools.js +1 -0
- package/dist/tools/register-spec-tools/core-spec-tools.js +16 -0
- package/dist/tools/spec-lock-handler.js +1 -1
- package/dist/tools/tool-registry/group-misc.js +4 -4
- package/dist/tools/update-status/batch.d.ts +3 -0
- package/dist/tools/update-status/batch.js +96 -0
- package/dist/tools/update-status/dod-gates.js +1 -1
- package/dist/tools/update-status/file-sync.js +3 -1
- package/dist/tools/update-status/index.js +15 -2
- package/dist/tools/update-status-actions.js +2 -6
- package/dist/tools/validate.js +27 -0
- package/dist/tools/workspace-dashboard-handler.js +6 -9
- package/dist/types/git.d.ts +1 -1
- package/dist/types/spec-format.d.ts +26 -0
- package/dist/types/spec-language.d.ts +8 -0
- package/dist/types/spec-language.js +2 -0
- package/package.json +20 -20
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
## [4.1.0] - 2026-05-21
|
|
2
|
+
|
|
3
|
+
### Features
|
|
4
|
+
- Enforce English-only persisted Planu artifacts across specs, skills, agent instructions, and rules.
|
|
5
|
+
- Gate host-aware init scaffolding for `AGENTS.md`, `CLAUDE.md`, Cursor, Windsurf, Cline, Gemini, Codex, and OpenCode generated AI docs.
|
|
6
|
+
- Keep user-authored host file content intact while validating only Planu-owned generated blocks.
|
|
7
|
+
|
|
8
|
+
### Tests
|
|
9
|
+
- Add coverage for skill, rule, and agent instruction language gates across core writers and host generators.
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
## [4.0.0] - 2026-05-20
|
|
13
|
+
|
|
14
|
+
**Tarball SHA-256:** `8c00d74f48ed5614197000a967b103cc17653150aadf876fcfd18d0174263017`
|
|
15
|
+
|
|
16
|
+
|
|
1
17
|
## [3.9.12] - 2026-05-19
|
|
2
18
|
|
|
3
19
|
**Tarball SHA-256:** `cd07a22fdfc0c982726a918c1e47f147ca300ecad710e8377f1751ef993fea60`
|
|
@@ -3787,4 +3803,4 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) · Versioning:
|
|
|
3787
3803
|
- Mermaid diagram generation (architecture, sequence, state machine, ER, data flow)
|
|
3788
3804
|
- Multi-language i18n (EN/ES/PT) for generated specs
|
|
3789
3805
|
- Clean Architecture (hexagonal) — engine, tools, storage, types layers
|
|
3790
|
-
- 10,857 tests with ≥95% coverage
|
|
3806
|
+
- 10,857 tests with ≥95% coverage
|
|
@@ -10,6 +10,7 @@ import { resolve } from 'node:path';
|
|
|
10
10
|
import { handleCreateSpec } from '../../tools/create-spec.js';
|
|
11
11
|
import { handleListSpecs } from '../../tools/list-specs.js';
|
|
12
12
|
import { handleUpdateStatus } from '../../tools/update-status.js';
|
|
13
|
+
import { handleUpdateStatusBatch } from '../../tools/update-status/batch.js';
|
|
13
14
|
import { formatToolResult } from '../formatter.js';
|
|
14
15
|
import { detectProjectId } from '../project-detector.js';
|
|
15
16
|
import { bold, cyan, green, red, dim } from '../colors.js';
|
|
@@ -178,12 +179,15 @@ async function runStatus(args, flags) {
|
|
|
178
179
|
options: {
|
|
179
180
|
'project-id': { type: 'string' },
|
|
180
181
|
notes: { type: 'string', short: 'n' },
|
|
182
|
+
batch: { type: 'boolean' },
|
|
183
|
+
set: { type: 'string', short: 's' },
|
|
181
184
|
},
|
|
182
185
|
strict: false,
|
|
183
186
|
allowPositionals: true,
|
|
184
187
|
});
|
|
188
|
+
const batch = values.batch === true;
|
|
185
189
|
const specId = positionals[0];
|
|
186
|
-
const status = positionals[1];
|
|
190
|
+
const status = batch ? values.set : positionals[1];
|
|
187
191
|
if (!specId) {
|
|
188
192
|
process.stderr.write(`${red('Error:')} Spec ID is required.\nUsage: planu spec status SPEC-NNN <status>\n`);
|
|
189
193
|
process.exitCode = 1;
|
|
@@ -195,6 +199,21 @@ async function runStatus(args, flags) {
|
|
|
195
199
|
return;
|
|
196
200
|
}
|
|
197
201
|
const projectId = values['project-id'] ?? detectProjectId();
|
|
202
|
+
if (batch) {
|
|
203
|
+
const result = await handleUpdateStatusBatch({
|
|
204
|
+
specIds: positionals,
|
|
205
|
+
projectId,
|
|
206
|
+
status: status,
|
|
207
|
+
reviewNotes: values.notes ?? undefined,
|
|
208
|
+
});
|
|
209
|
+
if (result.isError) {
|
|
210
|
+
process.stderr.write(`${red(formatToolResult(result, flags))}\n`);
|
|
211
|
+
process.exitCode = 1;
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
process.stdout.write(formatToolResult(result, flags) + '\n');
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
198
217
|
const result = await handleUpdateStatus({
|
|
199
218
|
specId,
|
|
200
219
|
projectId,
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
// cli/commands/status.ts — planu status <specId> [--set implementing] (SPEC-124)
|
|
2
2
|
import { parseArgs } from 'node:util';
|
|
3
3
|
import { handleUpdateStatus } from '../../tools/update-status.js';
|
|
4
|
+
import { handleUpdateStatusBatch } from '../../tools/update-status/batch.js';
|
|
4
5
|
import { formatToolResult } from '../formatter.js';
|
|
5
6
|
import { detectProjectId } from '../project-detector.js';
|
|
6
7
|
import { red, green } from '../colors.js';
|
|
7
8
|
export const statusCommand = {
|
|
8
9
|
name: 'status',
|
|
9
10
|
description: 'Update the status of a spec',
|
|
10
|
-
usage: 'planu status <specId> --set <status> [--project-id ID]',
|
|
11
|
+
usage: 'planu status <specId> --set <status> [--project-id ID] | planu status batch --set <status> SPEC-001 SPEC-002',
|
|
11
12
|
async run(args, flags) {
|
|
12
13
|
const { values, positionals } = parseArgs({
|
|
13
14
|
args,
|
|
@@ -19,6 +20,7 @@ export const statusCommand = {
|
|
|
19
20
|
strict: false,
|
|
20
21
|
allowPositionals: true,
|
|
21
22
|
});
|
|
23
|
+
const isBatch = positionals[0] === 'batch';
|
|
22
24
|
const specId = positionals[0];
|
|
23
25
|
if (!specId) {
|
|
24
26
|
process.stderr.write(`${red('Error:')} Spec ID is required.\nUsage: ${statusCommand.usage}\n`);
|
|
@@ -32,6 +34,21 @@ export const statusCommand = {
|
|
|
32
34
|
return;
|
|
33
35
|
}
|
|
34
36
|
const projectId = values['project-id'] ?? detectProjectId();
|
|
37
|
+
if (isBatch) {
|
|
38
|
+
const result = await handleUpdateStatusBatch({
|
|
39
|
+
specIds: positionals.slice(1),
|
|
40
|
+
projectId,
|
|
41
|
+
status: status,
|
|
42
|
+
reviewNotes: values.notes ?? undefined,
|
|
43
|
+
});
|
|
44
|
+
if (result.isError) {
|
|
45
|
+
process.stderr.write(`${red(formatToolResult(result, flags))}\n`);
|
|
46
|
+
process.exitCode = 1;
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
process.stdout.write(formatToolResult(result, flags) + '\n');
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
35
52
|
const result = await handleUpdateStatus({
|
|
36
53
|
specId,
|
|
37
54
|
projectId,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// engine/ai-integration/agents-md/generator.ts — Universal AGENTS.md generator (SPEC-269)
|
|
2
2
|
import { writeFileSync, readFileSync, existsSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
+
import { assertEnglishOnlyArtifactText } from '../../spec-language/english-only.js';
|
|
4
5
|
const PLANU_SECTION_START = '<!-- planu-sdd:start -->';
|
|
5
6
|
const PLANU_SECTION_END = '<!-- planu-sdd:end -->';
|
|
6
7
|
function buildActiveSpecBlock(activeSpecId, activeSpecTitle) {
|
|
@@ -105,7 +106,9 @@ function buildSddSection(options) {
|
|
|
105
106
|
parts.push('', approvedQueueBlock);
|
|
106
107
|
}
|
|
107
108
|
parts.push('', toolsBlock, '', branchBlock, '', architectureBlock, '', PLANU_SECTION_END);
|
|
108
|
-
|
|
109
|
+
const content = parts.join('\n');
|
|
110
|
+
assertEnglishOnlyArtifactText(content, 'agent');
|
|
111
|
+
return content;
|
|
109
112
|
}
|
|
110
113
|
/** Generates the full universal AGENTS.md content for a project using Planu SDD. */
|
|
111
114
|
export function generateUniversalAgentsMd(options) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// engine/ai-integration/cline/clinerules-generator.ts — .clinerules generator (SPEC-271)
|
|
2
2
|
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
+
import { assertEnglishOnlyArtifactText } from '../../spec-language/english-only.js';
|
|
4
5
|
const SECTION_START = '<!-- planu-sdd:start -->';
|
|
5
6
|
const SECTION_END = '<!-- planu-sdd:end -->';
|
|
6
7
|
function buildPlanuSection(options) {
|
|
@@ -52,12 +53,16 @@ function buildPlanuSection(options) {
|
|
|
52
53
|
lines.push('Place in `.clinerules` at project root or in `.clinerules/` directory.');
|
|
53
54
|
lines.push('');
|
|
54
55
|
lines.push(SECTION_END);
|
|
55
|
-
|
|
56
|
+
const content = lines.join('\n');
|
|
57
|
+
assertEnglishOnlyArtifactText(content, 'agent');
|
|
58
|
+
return content;
|
|
56
59
|
}
|
|
57
60
|
export function generateClineRules(options) {
|
|
58
61
|
const stackStr = options.stack.length > 0 ? options.stack.join(', ') : 'Not specified';
|
|
59
62
|
const header = [`# Cline Rules — ${options.projectName}`, '', `Stack: ${stackStr}`, ''].join('\n');
|
|
60
|
-
|
|
63
|
+
const content = header + buildPlanuSection(options);
|
|
64
|
+
assertEnglishOnlyArtifactText(content, 'agent');
|
|
65
|
+
return content;
|
|
61
66
|
}
|
|
62
67
|
export function writeClineRules(projectPath, options) {
|
|
63
68
|
const filePath = join(projectPath, '.clinerules');
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { writeFileSync, mkdirSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
+
import { assertEnglishOnlyArtifactText } from '../../spec-language/english-only.js';
|
|
3
4
|
const PLANU_SECTION_START = '<!-- planu:start -->';
|
|
4
5
|
const PLANU_SECTION_END = '<!-- planu:end -->';
|
|
5
6
|
function buildWorkflowSection() {
|
|
@@ -91,6 +92,7 @@ export function generateAgentsMd(options) {
|
|
|
91
92
|
architectureSection,
|
|
92
93
|
PLANU_SECTION_END,
|
|
93
94
|
].join('\n');
|
|
95
|
+
assertEnglishOnlyArtifactText(planuBlock, 'agent');
|
|
94
96
|
return planuBlock;
|
|
95
97
|
}
|
|
96
98
|
/**
|
|
@@ -8,6 +8,7 @@ const SESSION_START_COMMAND = [
|
|
|
8
8
|
].join(' ');
|
|
9
9
|
const STOP_COMMAND = [
|
|
10
10
|
'if [ -d "planu/" ]; then',
|
|
11
|
+
' if [ "$PLANU_ENABLE_AUTOCOMMIT" != "true" ]; then exit 0; fi;',
|
|
11
12
|
' git add planu/specs/ planu/conventions.json 2>/dev/null;',
|
|
12
13
|
' git diff --staged --quiet || git commit -m "chore: auto-save planu state" 2>/dev/null;',
|
|
13
14
|
'fi',
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// engine/ai-integration/cursor/cursorrules-generator.ts — .cursorrules generator (SPEC-268)
|
|
2
2
|
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
+
import { assertEnglishOnlyArtifactText } from '../../spec-language/english-only.js';
|
|
4
5
|
const SECTION_START = '<!-- planu-sdd:start -->';
|
|
5
6
|
const SECTION_END = '<!-- planu-sdd:end -->';
|
|
6
7
|
function buildPlanuSection(options) {
|
|
@@ -46,12 +47,16 @@ function buildPlanuSection(options) {
|
|
|
46
47
|
lines.push('');
|
|
47
48
|
}
|
|
48
49
|
lines.push(SECTION_END);
|
|
49
|
-
|
|
50
|
+
const content = lines.join('\n');
|
|
51
|
+
assertEnglishOnlyArtifactText(content, 'agent');
|
|
52
|
+
return content;
|
|
50
53
|
}
|
|
51
54
|
export function generateCursorRules(options) {
|
|
52
55
|
const stackStr = options.stack.length > 0 ? options.stack.join(', ') : 'Not specified';
|
|
53
56
|
const header = [`# Cursor Rules — ${options.projectName}`, '', `Stack: ${stackStr}`, ''].join('\n');
|
|
54
|
-
|
|
57
|
+
const content = header + buildPlanuSection(options);
|
|
58
|
+
assertEnglishOnlyArtifactText(content, 'agent');
|
|
59
|
+
return content;
|
|
55
60
|
}
|
|
56
61
|
export function writeCursorRules(projectPath, options) {
|
|
57
62
|
const filePath = join(projectPath, '.cursorrules');
|
|
@@ -17,7 +17,10 @@ const HOOK_SCRIPT_SESSION_START = [
|
|
|
17
17
|
].join('\n');
|
|
18
18
|
const HOOK_SCRIPT_SESSION_END = [
|
|
19
19
|
'#!/usr/bin/env bash',
|
|
20
|
-
'# session-end.sh — auto-commit planu/ changes',
|
|
20
|
+
'# session-end.sh — optionally auto-commit planu/ changes',
|
|
21
|
+
'if [ "$PLANU_ENABLE_AUTOCOMMIT" != "true" ]; then',
|
|
22
|
+
' exit 0',
|
|
23
|
+
'fi',
|
|
21
24
|
'if git diff --quiet HEAD -- planu/ 2>/dev/null; then',
|
|
22
25
|
' exit 0',
|
|
23
26
|
'fi',
|
|
@@ -18,6 +18,7 @@ const POST_TOOL_SCRIPT = [
|
|
|
18
18
|
const STOP_SCRIPT = [
|
|
19
19
|
'planu session_checkpoint --auto 2>/dev/null;',
|
|
20
20
|
'if [ -d "planu/" ]; then',
|
|
21
|
+
' if [ "$PLANU_ENABLE_AUTOCOMMIT" != "true" ]; then exit 0; fi;',
|
|
21
22
|
' git add planu/specs/ planu/conventions.json 2>/dev/null;',
|
|
22
23
|
' git diff --staged --quiet || git commit -m "chore: auto-save planu state [kiro]" 2>/dev/null;',
|
|
23
24
|
'fi',
|
|
@@ -40,7 +41,7 @@ export function generateKiroHooks() {
|
|
|
40
41
|
{
|
|
41
42
|
id: 'planu-stop',
|
|
42
43
|
event: 'stop',
|
|
43
|
-
description: 'Save session checkpoint and auto-commit planu state on session end',
|
|
44
|
+
description: 'Save session checkpoint and optionally auto-commit planu state on session end',
|
|
44
45
|
script: STOP_SCRIPT,
|
|
45
46
|
},
|
|
46
47
|
];
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// engine/ai-integration/windsurf/windsurfrules-generator.ts — .windsurfrules generator (SPEC-268)
|
|
2
2
|
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
+
import { assertEnglishOnlyArtifactText } from '../../spec-language/english-only.js';
|
|
4
5
|
const SECTION_START = '<!-- planu-sdd:start -->';
|
|
5
6
|
const SECTION_END = '<!-- planu-sdd:end -->';
|
|
6
7
|
function buildPlanuSection(options) {
|
|
@@ -46,12 +47,16 @@ function buildPlanuSection(options) {
|
|
|
46
47
|
lines.push('');
|
|
47
48
|
}
|
|
48
49
|
lines.push(SECTION_END);
|
|
49
|
-
|
|
50
|
+
const content = lines.join('\n');
|
|
51
|
+
assertEnglishOnlyArtifactText(content, 'agent');
|
|
52
|
+
return content;
|
|
50
53
|
}
|
|
51
54
|
export function generateWindsurfRules(options) {
|
|
52
55
|
const stackStr = options.stack.length > 0 ? options.stack.join(', ') : 'Not specified';
|
|
53
56
|
const header = [`# Windsurf Rules — ${options.projectName}`, '', `Stack: ${stackStr}`, ''].join('\n');
|
|
54
|
-
|
|
57
|
+
const content = header + buildPlanuSection(options);
|
|
58
|
+
assertEnglishOnlyArtifactText(content, 'agent');
|
|
59
|
+
return content;
|
|
55
60
|
}
|
|
56
61
|
export function writeWindsurfRules(projectPath, options) {
|
|
57
62
|
const filePath = join(projectPath, '.windsurfrules');
|
|
@@ -202,25 +202,16 @@ const ACTION_HANDLERS = {
|
|
|
202
202
|
const specs = await specStore.listSpecs(ctx.projectId);
|
|
203
203
|
const { generateChangelog } = await import('../doc-generator/support-generators.js');
|
|
204
204
|
const doc = generateChangelog(specs, 'en');
|
|
205
|
-
//
|
|
205
|
+
// Generated changelog is returned to callers; strict Planu mode forbids planu/CHANGELOG.md.
|
|
206
206
|
const { writeFile, mkdir } = await import('node:fs/promises');
|
|
207
207
|
const { join } = await import('node:path');
|
|
208
|
-
const
|
|
209
|
-
|
|
208
|
+
const { projectDataDir } = await import('../../storage/base-store.js');
|
|
209
|
+
const changelogPath = join(projectDataDir(ctx.projectId), 'generated', 'CHANGELOG.md');
|
|
210
|
+
await mkdir(join(projectDataDir(ctx.projectId), 'generated'), { recursive: true });
|
|
210
211
|
await writeFile(changelogPath, doc.content, 'utf-8');
|
|
211
|
-
// Auto-commit planu/ changes so CHANGELOG.md is never left unstaged
|
|
212
|
-
void (async () => {
|
|
213
|
-
try {
|
|
214
|
-
const { planuAutoCommit } = await import('../git/planu-autocommit.js');
|
|
215
|
-
await planuAutoCommit({ projectPath: ctx.projectPath, reason: 'sync-release' });
|
|
216
|
-
}
|
|
217
|
-
catch {
|
|
218
|
-
/* best-effort */
|
|
219
|
-
}
|
|
220
|
-
})();
|
|
221
212
|
return {
|
|
222
213
|
success: true,
|
|
223
|
-
summary: `Changelog written to
|
|
214
|
+
summary: `Changelog written to external Planu project data (${doc.content.length} chars)`,
|
|
224
215
|
durationMs: 0,
|
|
225
216
|
};
|
|
226
217
|
}),
|
|
@@ -1,26 +1,29 @@
|
|
|
1
1
|
// @crash-shield-ignore-file — config/cache reader for Planu-controlled JSON; writer is this codebase, shape guaranteed by build/seed.
|
|
2
|
-
// engine/autopilot/state-updater.ts — SPEC-459: Auto-update
|
|
2
|
+
// engine/autopilot/state-updater.ts — SPEC-459: Auto-update external project status on every status change
|
|
3
3
|
// Fire-and-forget: never blocks the event pipeline, never throws to callers.
|
|
4
4
|
// SPEC-753: File-lock applied to all writes; self-healing on corrupt reads.
|
|
5
5
|
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
6
6
|
import { join, dirname } from 'node:path';
|
|
7
7
|
import { withStatusLock } from '../../storage/status-store/file-lock.js';
|
|
8
|
-
import { validateStatusJson,
|
|
8
|
+
import { validateStatusJson, rebuildStatusFromFrontmatters, writeRebuiltStatus, } from '../../storage/status-store/self-healing.js';
|
|
9
9
|
import { syncVersionField } from '../../storage/status-store/version-sync.js';
|
|
10
|
-
|
|
10
|
+
import { hashProjectPath, projectDataDir } from '../../storage/base-store.js';
|
|
11
|
+
const STATUS_FILENAME = 'status.json';
|
|
11
12
|
const MAX_RECENT_CHANGES = 20;
|
|
13
|
+
function getStatusPath(projectPath) {
|
|
14
|
+
return join(projectDataDir(hashProjectPath(projectPath)), STATUS_FILENAME);
|
|
15
|
+
}
|
|
12
16
|
/**
|
|
13
17
|
* SPEC-753: Load status.json with self-healing.
|
|
14
18
|
* If corrupt: quarantine → rebuild from frontmatters → return fresh state.
|
|
15
19
|
*/
|
|
16
20
|
async function loadStatus(projectPath) {
|
|
17
|
-
const filePath =
|
|
21
|
+
const filePath = getStatusPath(projectPath);
|
|
18
22
|
// SPEC-753: Validate before parsing
|
|
19
23
|
const validation = await validateStatusJson(filePath);
|
|
20
24
|
if (!validation.ok && validation.error !== 'ENOENT') {
|
|
21
|
-
// Corrupt
|
|
25
|
+
// Corrupt external state is rebuilt in place; no project-tree quarantine.
|
|
22
26
|
try {
|
|
23
|
-
await quarantineCorruptStatus(filePath, projectPath);
|
|
24
27
|
const rebuilt = await rebuildStatusFromFrontmatters({ projectPath });
|
|
25
28
|
await writeRebuiltStatus(filePath, rebuilt);
|
|
26
29
|
return rebuilt;
|
|
@@ -48,7 +51,7 @@ async function loadStatus(projectPath) {
|
|
|
48
51
|
}
|
|
49
52
|
}
|
|
50
53
|
async function saveStatus(projectPath, status) {
|
|
51
|
-
const filePath =
|
|
54
|
+
const filePath = getStatusPath(projectPath);
|
|
52
55
|
await mkdir(dirname(filePath), { recursive: true });
|
|
53
56
|
await writeFile(filePath, JSON.stringify(status, null, 2), 'utf-8');
|
|
54
57
|
}
|
|
@@ -58,7 +61,7 @@ async function saveStatus(projectPath, status) {
|
|
|
58
61
|
* SPEC-753: Wrapped in file-lock to prevent concurrent write corruption.
|
|
59
62
|
*/
|
|
60
63
|
export async function recordStatusChange(projectPath, _projectId, specId, fromStatus, toStatus) {
|
|
61
|
-
const filePath =
|
|
64
|
+
const filePath = getStatusPath(projectPath);
|
|
62
65
|
try {
|
|
63
66
|
await withStatusLock(filePath, async () => {
|
|
64
67
|
const status = await loadStatus(projectPath);
|
|
@@ -91,7 +94,7 @@ export async function recordStatusChange(projectPath, _projectId, specId, fromSt
|
|
|
91
94
|
* SPEC-753: Wrapped in file-lock.
|
|
92
95
|
*/
|
|
93
96
|
export async function refreshProjectStatus(projectPath, projectId) {
|
|
94
|
-
const filePath =
|
|
97
|
+
const filePath = getStatusPath(projectPath);
|
|
95
98
|
try {
|
|
96
99
|
await withStatusLock(filePath, async () => {
|
|
97
100
|
const { specStore } = await import('../../storage/index.js');
|
|
@@ -124,7 +127,7 @@ export async function refreshProjectStatus(projectPath, projectId) {
|
|
|
124
127
|
* SPEC-753: Wrapped in file-lock.
|
|
125
128
|
*/
|
|
126
129
|
export async function incrementSpecCount(projectPath) {
|
|
127
|
-
const filePath =
|
|
130
|
+
const filePath = getStatusPath(projectPath);
|
|
128
131
|
try {
|
|
129
132
|
await withStatusLock(filePath, async () => {
|
|
130
133
|
const status = await loadStatus(projectPath);
|
|
@@ -5,6 +5,9 @@ async function handler(ctx) {
|
|
|
5
5
|
if (!projectPath) {
|
|
6
6
|
return;
|
|
7
7
|
}
|
|
8
|
+
if (process.env.PLANU_ENABLE_AUTOCOMMIT !== 'true') {
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
8
11
|
// Guard: only auto-stage when validateScore is non-null (matches original inline guard)
|
|
9
12
|
if (validateScore === null) {
|
|
10
13
|
return;
|
|
@@ -13,7 +13,7 @@ export const htmlRegenHook = {
|
|
|
13
13
|
id: 'html-regen',
|
|
14
14
|
triggers: ['done'], // only on done
|
|
15
15
|
requiresProjectPath: true,
|
|
16
|
-
label: '
|
|
16
|
+
label: 'Run legacy dashboard compatibility hook without writing project HTML',
|
|
17
17
|
handler,
|
|
18
18
|
};
|
|
19
19
|
//# sourceMappingURL=html-regen.hook.js.map
|
|
@@ -28,7 +28,7 @@ export const statusJsonHook = {
|
|
|
28
28
|
id: 'status-json',
|
|
29
29
|
triggers: [], // any status change + release_completed
|
|
30
30
|
requiresProjectPath: true,
|
|
31
|
-
label: 'Auto-update
|
|
31
|
+
label: 'Auto-update external project status with spec counts (SPEC-459/776)',
|
|
32
32
|
handler,
|
|
33
33
|
};
|
|
34
34
|
//# sourceMappingURL=status-json.hook.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { DriftReport } from '../../types/cascade-hooks.js';
|
|
2
2
|
/**
|
|
3
|
-
* SPEC-776: Compare in-memory spec counts vs
|
|
3
|
+
* SPEC-776: Compare in-memory spec counts vs external status.json and
|
|
4
4
|
* recently-done spec vs planu/session-context.md.
|
|
5
5
|
* Returns a DriftReport with any detected mismatches.
|
|
6
6
|
*/
|
|
@@ -1,32 +1,35 @@
|
|
|
1
1
|
// engine/cascade-hooks/state-drift-detector.ts — SPEC-776
|
|
2
|
-
// Detects drift between in-memory spec store and
|
|
2
|
+
// Detects drift between in-memory spec store and canonical Planu state files.
|
|
3
3
|
import { readFile } from 'node:fs/promises';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { specStore } from '../../storage/index.js';
|
|
6
|
-
import { hashProjectPath } from '../../storage/base-store.js';
|
|
7
|
-
|
|
6
|
+
import { hashProjectPath, projectDataDir } from '../../storage/base-store.js';
|
|
7
|
+
function externalStatusPath(projectPath) {
|
|
8
|
+
return join(projectDataDir(hashProjectPath(projectPath)), 'status.json');
|
|
9
|
+
}
|
|
10
|
+
/** Read and parse external status counts; returns null if missing or invalid. */
|
|
8
11
|
async function readStatusJsonDoneCounts(projectPath) {
|
|
9
12
|
try {
|
|
10
|
-
const raw = await readFile(
|
|
13
|
+
const raw = await readFile(externalStatusPath(projectPath), 'utf-8');
|
|
11
14
|
const parsed = JSON.parse(raw);
|
|
12
15
|
if (parsed === null || typeof parsed !== 'object') {
|
|
13
16
|
return null;
|
|
14
17
|
}
|
|
15
|
-
const
|
|
16
|
-
if (
|
|
18
|
+
const byStatus = parsed.byStatus;
|
|
19
|
+
if (byStatus === null || typeof byStatus !== 'object') {
|
|
17
20
|
return null;
|
|
18
21
|
}
|
|
19
|
-
const doneVal =
|
|
22
|
+
const doneVal = byStatus.done;
|
|
20
23
|
return typeof doneVal === 'number' ? doneVal : null;
|
|
21
24
|
}
|
|
22
25
|
catch {
|
|
23
26
|
return null;
|
|
24
27
|
}
|
|
25
28
|
}
|
|
26
|
-
/** Returns true if
|
|
29
|
+
/** Returns true if external status.json exists (even if counts are missing). */
|
|
27
30
|
async function statusJsonExists(projectPath) {
|
|
28
31
|
try {
|
|
29
|
-
await readFile(
|
|
32
|
+
await readFile(externalStatusPath(projectPath), 'utf-8');
|
|
30
33
|
return true;
|
|
31
34
|
}
|
|
32
35
|
catch {
|
|
@@ -57,7 +60,7 @@ function extractLastDoneSpecFromContext(content) {
|
|
|
57
60
|
return idMatch ? idMatch[0] : null;
|
|
58
61
|
}
|
|
59
62
|
/**
|
|
60
|
-
* SPEC-776: Compare in-memory spec counts vs
|
|
63
|
+
* SPEC-776: Compare in-memory spec counts vs external status.json and
|
|
61
64
|
* recently-done spec vs planu/session-context.md.
|
|
62
65
|
* Returns a DriftReport with any detected mismatches.
|
|
63
66
|
*/
|
|
@@ -72,12 +75,12 @@ export async function verifyStateFiles(projectPath) {
|
|
|
72
75
|
return { drifted: false, alerts: [] };
|
|
73
76
|
}
|
|
74
77
|
const expectedDoneCount = specs.filter((s) => s.status === 'done').length;
|
|
75
|
-
// --- Check
|
|
78
|
+
// --- Check external status.json ---
|
|
76
79
|
const exists = await statusJsonExists(projectPath);
|
|
77
80
|
if (!exists) {
|
|
78
81
|
alerts.push({
|
|
79
82
|
kind: 'state_drift',
|
|
80
|
-
message: 'state_drift:
|
|
83
|
+
message: 'state_drift: external status.json missing or unreadable',
|
|
81
84
|
severity: 'warning',
|
|
82
85
|
fix: 'reconcile_status_json(projectPath)',
|
|
83
86
|
});
|
|
@@ -4,6 +4,9 @@ import { promisify } from 'node:util';
|
|
|
4
4
|
import { access } from 'node:fs/promises';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
const execFile = promisify(execFileCb);
|
|
7
|
+
export function isPlanuAutocommitEnabled() {
|
|
8
|
+
return process.env.PLANU_ENABLE_AUTOCOMMIT === 'true';
|
|
9
|
+
}
|
|
7
10
|
/** Run git with execFile (no shell injection risk). */
|
|
8
11
|
async function runGit(cwd, args) {
|
|
9
12
|
const result = await execFile('git', args, {
|
|
@@ -101,6 +104,9 @@ async function unstageFiles(projectPath) {
|
|
|
101
104
|
*/
|
|
102
105
|
export async function planuAutoCommit(opts) {
|
|
103
106
|
const { projectPath, specId, reason } = opts;
|
|
107
|
+
if (!isPlanuAutocommitEnabled()) {
|
|
108
|
+
return { committed: false, skipped: 'disabled' };
|
|
109
|
+
}
|
|
104
110
|
// 1. Safety check: refuse if mid-merge
|
|
105
111
|
const midMerge = await isMidMerge(projectPath);
|
|
106
112
|
if (midMerge) {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// SPEC-347: auto-stage planu HTML files so they are never left out of commits
|
|
4
4
|
import { readFile, writeFile, access, constants } from 'node:fs/promises';
|
|
5
5
|
import { join, resolve } from 'node:path';
|
|
6
|
-
const PLANU_SNIPPET = `\n# Planu: auto-stage specs (SPEC-466 — only source-of-truth files)\
|
|
6
|
+
const PLANU_SNIPPET = `\n# Planu: optional auto-stage specs (SPEC-466 — only source-of-truth files)\nif [ "$PLANU_ENABLE_AUTOCOMMIT" = "true" ]; then\n git add planu/specs/ planu/conventions.json 2>/dev/null || true\nfi\n`;
|
|
7
7
|
const PLANU_MARKER = 'git add planu/';
|
|
8
8
|
async function fileExists(filePath) {
|
|
9
9
|
try {
|
|
@@ -74,8 +74,8 @@ export async function injectPlanuAutoStage(projectPath) {
|
|
|
74
74
|
}
|
|
75
75
|
// lefthook / simple-git-hooks: cannot auto-inject, return manual instructions
|
|
76
76
|
const manualInstructions = hookSystem === 'lefthook'
|
|
77
|
-
? `Add to lefthook.yml under pre-commit commands:\n - run: git add planu/specs/ planu/conventions.json 2>/dev/null || true`
|
|
78
|
-
: `Add to package.json "simple-git-hooks"."pre-commit":\n "git add planu/specs/ planu/conventions.json 2>/dev/null || true"`;
|
|
77
|
+
? `Add to lefthook.yml under pre-commit commands:\n - run: '[ "$PLANU_ENABLE_AUTOCOMMIT" = "true" ] && git add planu/specs/ planu/conventions.json 2>/dev/null || true'`
|
|
78
|
+
: `Add to package.json "simple-git-hooks"."pre-commit":\n "[ \\"$PLANU_ENABLE_AUTOCOMMIT\\" = \\"true\\" ] && git add planu/specs/ planu/conventions.json 2>/dev/null || true"`;
|
|
79
79
|
return {
|
|
80
80
|
injected: false,
|
|
81
81
|
hookSystem,
|
|
@@ -7,6 +7,7 @@ import { acquireLock, releaseLock } from '../safety/cross-process-lock.js';
|
|
|
7
7
|
import { appendTransitionEvent } from '../../storage/transition-log.js';
|
|
8
8
|
import { validateArtifact } from './schemas.js';
|
|
9
9
|
import { resolveCompat } from './version-policy.js';
|
|
10
|
+
import { projectDataDir } from '../../storage/base-store.js';
|
|
10
11
|
// Current schema version for version-policy checks
|
|
11
12
|
const CURRENT_SCHEMA_VERSION = '1.0.0';
|
|
12
13
|
// ---------------------------------------------------------------------------
|
|
@@ -14,14 +15,14 @@ const CURRENT_SCHEMA_VERSION = '1.0.0';
|
|
|
14
15
|
// ---------------------------------------------------------------------------
|
|
15
16
|
/**
|
|
16
17
|
* Returns the file path for a handoff artifact.
|
|
17
|
-
* Layout: planu/data/projects/<projectId>/handoffs/<specId>/<kind>
|
|
18
|
+
* Layout: ~/.planu/data/projects/<projectId>/handoffs/<specId>/<kind>
|
|
18
19
|
*
|
|
19
20
|
* Note: kind values like 'spec.lock', 'review_feedback', etc. are used directly as filenames.
|
|
20
21
|
*/
|
|
21
22
|
function artifactPath(projectId, specId, kind) {
|
|
22
23
|
// review_feedback is a .md file, rest are .json
|
|
23
24
|
const filename = kind === 'review_feedback' ? 'review_feedback.md' : `${kind}.json`;
|
|
24
|
-
return join(
|
|
25
|
+
return join(projectDataDir(projectId), 'handoffs', specId, filename);
|
|
25
26
|
}
|
|
26
27
|
/** Map artifact kind to the transition-log eventType */
|
|
27
28
|
function eventTypeForKind(kind) {
|
|
@@ -3,6 +3,7 @@ import { createHash } from 'node:crypto';
|
|
|
3
3
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { stripFrontmatter } from './frontmatter-parser.js';
|
|
6
|
+
import { hashProjectPath, projectDataDir } from '../storage/base-store.js';
|
|
6
7
|
// ── Parsing helpers ──────────────────────────────────────────────────────────
|
|
7
8
|
async function safeReadFile(path) {
|
|
8
9
|
if (!path) {
|
|
@@ -293,7 +294,7 @@ async function persistHandoffIfPossible(pkg, knowledge) {
|
|
|
293
294
|
if (!knowledge.projectPath) {
|
|
294
295
|
return pkg;
|
|
295
296
|
}
|
|
296
|
-
const handoffDir = join(knowledge.projectPath, '
|
|
297
|
+
const handoffDir = join(projectDataDir(hashProjectPath(knowledge.projectPath)), 'handoffs');
|
|
297
298
|
const handoffPath = join(handoffDir, `${pkg.specId}.md`);
|
|
298
299
|
const sessionContextPath = join(knowledge.projectPath, 'planu', 'session-context.md');
|
|
299
300
|
const markdown = renderPersistedHandoff(pkg);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { FullSpectrumHookConfig, HookScript } from '../../types/hooks-advanced.js';
|
|
2
2
|
/**
|
|
3
|
-
* Generates the Stop hook script
|
|
3
|
+
* Generates the Stop hook script. Planu housekeeping is opt-in so generated
|
|
4
|
+
* hooks cannot silently stage or commit user branches.
|
|
4
5
|
*/
|
|
5
6
|
export declare function generateStopHookScript(projectPath: string): string;
|
|
6
7
|
/**
|
|
@@ -3,12 +3,14 @@
|
|
|
3
3
|
// Individual script builders
|
|
4
4
|
// ---------------------------------------------------------------------------
|
|
5
5
|
/**
|
|
6
|
-
* Generates the Stop hook script
|
|
6
|
+
* Generates the Stop hook script. Planu housekeeping is opt-in so generated
|
|
7
|
+
* hooks cannot silently stage or commit user branches.
|
|
7
8
|
*/
|
|
8
9
|
export function generateStopHookScript(projectPath) {
|
|
9
10
|
return `#!/bin/bash
|
|
10
|
-
#
|
|
11
|
+
# Optionally auto-commit planu/ if explicitly enabled (Planu Stop hook)
|
|
11
12
|
cd "${projectPath}"
|
|
13
|
+
if [ "$PLANU_ENABLE_AUTOCOMMIT" != "true" ]; then exit 0; fi
|
|
12
14
|
if git diff --quiet planu/ 2>/dev/null; then exit 0; fi
|
|
13
15
|
git add planu/specs/ planu/conventions.json && git commit -m "chore(planu): auto-commit session state [skip ci]" --no-verify
|
|
14
16
|
`;
|
|
@@ -73,7 +75,7 @@ export function generateFullSpectrumHooks(config, projectPath = '$CLAUDE_PROJECT
|
|
|
73
75
|
scripts.push({
|
|
74
76
|
eventType: 'Stop',
|
|
75
77
|
scriptContent: generateStopHookScript(projectPath),
|
|
76
|
-
description: '
|
|
78
|
+
description: 'Optionally auto-commits planu/ changes at session end when enabled.',
|
|
77
79
|
});
|
|
78
80
|
}
|
|
79
81
|
if (config.preCompactHook) {
|