@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
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Wraps fetchAnthropicSkillContent from anthropic-adapter.ts and converts to FetchedSkill[].
|
|
3
3
|
import { mkdir, writeFile } from 'node:fs/promises';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
|
+
import { assertEnglishOnlyArtifactText } from '../spec-language/english-only.js';
|
|
5
6
|
import { fetchAnthropicSkillContent } from '../skill-registry/anthropic-adapter.js';
|
|
6
7
|
import { readCache, writeCache } from './cache.js';
|
|
7
8
|
// ---------------------------------------------------------------------------
|
|
@@ -107,6 +108,7 @@ async function writeSkillFile(projectPath, skillName, content) {
|
|
|
107
108
|
const skillsDir = join(projectPath, '.claude', 'skills');
|
|
108
109
|
await mkdir(skillsDir, { recursive: true });
|
|
109
110
|
const filePath = join(skillsDir, `${skillName}.md`);
|
|
111
|
+
assertEnglishOnlyArtifactText(`${skillName}\n\n${content}`, 'skill');
|
|
110
112
|
await writeFile(filePath, content, 'utf-8');
|
|
111
113
|
return filePath;
|
|
112
114
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// src/engine/opencode/config-scaffold.ts — SPEC-966: Generate OpenCode config files
|
|
2
2
|
import { mkdirSync, writeFileSync, existsSync } from 'fs';
|
|
3
3
|
import { join } from 'path';
|
|
4
|
+
import { assertEnglishOnlyArtifactText } from '../spec-language/english-only.js';
|
|
4
5
|
const PLANU_OPENCODE_RULES = `# Planu Rules for OpenCode
|
|
5
6
|
|
|
6
7
|
## SDD Workflow
|
|
@@ -73,16 +74,19 @@ export function scaffoldOpenCodeConfig(projectPath) {
|
|
|
73
74
|
const rulesDir = join(projectPath, '.opencode', 'rules');
|
|
74
75
|
mkdirSync(rulesDir, { recursive: true });
|
|
75
76
|
const rulesPath = join(rulesDir, 'planu-workflow.md');
|
|
77
|
+
assertEnglishOnlyArtifactText(PLANU_OPENCODE_RULES, 'rule');
|
|
76
78
|
writeFileSync(rulesPath, PLANU_OPENCODE_RULES, 'utf-8');
|
|
77
79
|
files.push(rulesPath);
|
|
78
80
|
// .opencode/skills/planu-sdd.md
|
|
79
81
|
const skillsDir = join(projectPath, '.opencode', 'skills');
|
|
80
82
|
mkdirSync(skillsDir, { recursive: true });
|
|
81
83
|
const skillsPath = join(skillsDir, 'planu-sdd.md');
|
|
84
|
+
assertEnglishOnlyArtifactText(PLANU_OPENCODE_SKILLS, 'skill');
|
|
82
85
|
writeFileSync(skillsPath, PLANU_OPENCODE_SKILLS, 'utf-8');
|
|
83
86
|
files.push(skillsPath);
|
|
84
87
|
// AGENTS.md append (or create)
|
|
85
88
|
const agentsMdPath = join(projectPath, 'AGENTS.md');
|
|
89
|
+
assertEnglishOnlyArtifactText(PLANU_AGENTS_MD_BLOCK, 'agent');
|
|
86
90
|
if (existsSync(agentsMdPath)) {
|
|
87
91
|
writeFileSync(agentsMdPath, '\n' + PLANU_AGENTS_MD_BLOCK + '\n', { flag: 'a' });
|
|
88
92
|
files.push(agentsMdPath + ' (appended)');
|
|
@@ -6,7 +6,7 @@ export interface PostmortemInput {
|
|
|
6
6
|
projectRoot?: string;
|
|
7
7
|
}
|
|
8
8
|
/**
|
|
9
|
-
* Write a post-mortem skeleton to
|
|
9
|
+
* Write a post-mortem skeleton to external Planu project data.
|
|
10
10
|
* Returns the path of the written file.
|
|
11
11
|
*/
|
|
12
12
|
export declare function writePostmortem(input: PostmortemInput): Promise<string>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// engine/release/postmortem-generator.ts — SPEC-737: Post-mortem skeleton writer
|
|
2
2
|
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
+
import { hashProjectPath, projectDataDir } from '../../storage/base-store.js';
|
|
4
5
|
// ---------------------------------------------------------------------------
|
|
5
6
|
// Template path
|
|
6
7
|
// ---------------------------------------------------------------------------
|
|
@@ -12,7 +13,7 @@ function templatePath() {
|
|
|
12
13
|
// Public API
|
|
13
14
|
// ---------------------------------------------------------------------------
|
|
14
15
|
/**
|
|
15
|
-
* Write a post-mortem skeleton to
|
|
16
|
+
* Write a post-mortem skeleton to external Planu project data.
|
|
16
17
|
* Returns the path of the written file.
|
|
17
18
|
*/
|
|
18
19
|
export async function writePostmortem(input) {
|
|
@@ -20,7 +21,7 @@ export async function writePostmortem(input) {
|
|
|
20
21
|
const isoNow = new Date().toISOString();
|
|
21
22
|
const tsSlug = isoNow.replace(/[:.]/g, '-').replace('T', '_').slice(0, 19);
|
|
22
23
|
const filename = `${version}-${tsSlug}.md`;
|
|
23
|
-
const outDir = join(projectRoot, '
|
|
24
|
+
const outDir = join(projectDataDir(hashProjectPath(projectRoot)), 'research', 'postmortems');
|
|
24
25
|
const outPath = join(outDir, filename);
|
|
25
26
|
// Load template
|
|
26
27
|
let template;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { detectAiTools, getPrimaryAiTool } from '../ai-tool-detector/index.js';
|
|
4
|
+
import { assertEnglishOnlyArtifactText } from '../spec-language/english-only.js';
|
|
4
5
|
// Stack-specific architecture rules catalog
|
|
5
6
|
export const ARCHITECTURE_RULES = {
|
|
6
7
|
nextjs: {
|
|
@@ -300,6 +301,7 @@ export function writeRules(rules) {
|
|
|
300
301
|
if (dir) {
|
|
301
302
|
mkdirSync(dir, { recursive: true });
|
|
302
303
|
}
|
|
304
|
+
assertEnglishOnlyArtifactText(rule.content, 'rule');
|
|
303
305
|
writeFileSync(rule.filePath, rule.content, 'utf-8');
|
|
304
306
|
written.push(rule.filePath);
|
|
305
307
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { readFileSync, readdirSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
3
3
|
import { join, basename } from 'node:path';
|
|
4
4
|
import { parseConventions } from './convention-scanner/convention-parser.js';
|
|
5
|
+
import { assertEnglishOnlyArtifactText } from './spec-language/english-only.js';
|
|
5
6
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
6
7
|
function listRulesFiles(projectPath) {
|
|
7
8
|
const rulesDir = join(projectPath, '.claude', 'rules');
|
|
@@ -122,6 +123,7 @@ function writeNewRulesFile(projectPath, category, conventions) {
|
|
|
122
123
|
const fileName = `${category}.md`;
|
|
123
124
|
const filePath = join(rulesDir, fileName);
|
|
124
125
|
const content = generateRuleFileContent(category, conventions);
|
|
126
|
+
assertEnglishOnlyArtifactText(content, 'rule');
|
|
125
127
|
writeFileSync(filePath, content, 'utf-8');
|
|
126
128
|
return fileName;
|
|
127
129
|
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// SMB / NFSv3 may produce false acquires because O_EXCL is not guaranteed atomic on
|
|
8
8
|
// network filesystems. For local-disk POSIX (HFS+, APFS, ext4, tmpfs) this is reliable.
|
|
9
9
|
//
|
|
10
|
-
// Lockfiles live at: <projectPath>/
|
|
10
|
+
// Lockfiles live at: <projectPath>/data/.locks/planu/<specId>.lock
|
|
11
11
|
// File mode: 0o600 (owner read/write only)
|
|
12
12
|
import { open, unlink, readFile, mkdir, chmod } from 'node:fs/promises';
|
|
13
13
|
import { existsSync } from 'node:fs';
|
|
@@ -41,7 +41,7 @@ export class LockBusyError extends Error {
|
|
|
41
41
|
// Internal helpers
|
|
42
42
|
// ---------------------------------------------------------------------------
|
|
43
43
|
function locksDir(projectPath) {
|
|
44
|
-
return join(projectPath, '
|
|
44
|
+
return join(projectPath, 'data', '.locks', 'planu');
|
|
45
45
|
}
|
|
46
46
|
function lockPath(projectPath, specId) {
|
|
47
47
|
return join(locksDir(projectPath), `${specId}.lock`);
|
|
@@ -74,7 +74,6 @@ export async function writeCheckpoint(projectPath, snapshot) {
|
|
|
74
74
|
const updated = replaceLatestCheckpoint(existing, newBlock);
|
|
75
75
|
await mkdir(planuDir, { recursive: true });
|
|
76
76
|
await writeFile(filePath, updated, 'utf8');
|
|
77
|
-
// Auto-commit planu/ changes so session-context.md is never left unstaged
|
|
78
77
|
void (async () => {
|
|
79
78
|
try {
|
|
80
79
|
const { planuAutoCommit } = await import('../git/planu-autocommit.js');
|
|
@@ -132,7 +132,10 @@ export async function generateSessionContext(projectPath, projectId) {
|
|
|
132
132
|
await mkdir(planuDir, { recursive: true });
|
|
133
133
|
const contextPath = join(planuDir, 'session-context.md');
|
|
134
134
|
await writeFile(contextPath, content, 'utf-8');
|
|
135
|
-
|
|
135
|
+
if (process.env.PLANU_ENABLE_AUTOCOMMIT !== 'true') {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
// SPEC-598: opt-in atomic commit for hosts that explicitly enable Planu autocommit.
|
|
136
139
|
try {
|
|
137
140
|
const { execFile } = await import('node:child_process');
|
|
138
141
|
const { promisify } = await import('node:util');
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// engine/skill-bootstrap/skill-writer.ts — SPEC-439: Write skills to target AI tool files
|
|
2
2
|
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
3
3
|
import { join, dirname } from 'node:path';
|
|
4
|
+
import { assertEnglishOnlyArtifactText } from '../spec-language/english-only.js';
|
|
4
5
|
const AUTO_SECTION_HEADER = '## Auto-installed Skills (Planu)';
|
|
5
6
|
function targetToRelativePath(target) {
|
|
6
7
|
switch (target) {
|
|
@@ -42,6 +43,7 @@ export function formatSkillsForTarget(skills, target) {
|
|
|
42
43
|
return parts.join('\n\n');
|
|
43
44
|
}
|
|
44
45
|
export async function writeSkillFile(projectPath, target, content, autoMerge) {
|
|
46
|
+
assertEnglishOnlyArtifactText(content, 'skill');
|
|
45
47
|
const relativePath = targetToRelativePath(target);
|
|
46
48
|
const filePath = join(projectPath, relativePath);
|
|
47
49
|
await mkdir(dirname(filePath), { recursive: true });
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Detects which AI agents are present in a project and writes adapted skill files.
|
|
3
3
|
import { access, mkdir, writeFile, readFile } from 'node:fs/promises';
|
|
4
4
|
import { join, dirname } from 'node:path';
|
|
5
|
+
import { assertEnglishOnlyArtifactText } from '../spec-language/english-only.js';
|
|
5
6
|
import { adaptSkillForAgent } from './multi-agent-adapter.js';
|
|
6
7
|
// ---------------------------------------------------------------------------
|
|
7
8
|
// Agent detection signals
|
|
@@ -92,6 +93,7 @@ async function writeNewFile(filePath, content) {
|
|
|
92
93
|
* @param targets - Optional explicit list of targets (default: auto-detect)
|
|
93
94
|
*/
|
|
94
95
|
export async function writeSkillToAllAgents(skillContent, metadata, projectPath, targets) {
|
|
96
|
+
assertEnglishOnlyArtifactText(`${metadata.name}\n\n${metadata.description}\n\n${skillContent}`, 'skill');
|
|
95
97
|
const resolvedTargets = targets ?? (await detectPresentAgents(projectPath));
|
|
96
98
|
const results = [];
|
|
97
99
|
await Promise.all(resolvedTargets.map(async (target) => {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Orchestrates the two-tier spec drift audit.
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { listSpecs } from '../../storage/spec-store.js';
|
|
5
|
-
import { hashProjectPath } from '../../storage/base-store.js';
|
|
5
|
+
import { hashProjectPath, projectDataDir } from '../../storage/base-store.js';
|
|
6
6
|
import { buildSpecGraph } from '../spec-dependency-graph/graph-builder.js';
|
|
7
7
|
import { getSupersededSet, getSupersededMap } from '../spec-dependency-graph/superseded-set.js';
|
|
8
8
|
import { scanSpecTier1 } from './tier1-scanner.js';
|
|
@@ -61,7 +61,7 @@ export async function auditSpecsDrift(opts) {
|
|
|
61
61
|
}
|
|
62
62
|
// Generate report path
|
|
63
63
|
const ts = new Date().toISOString().replace(/[:.]/g, '').slice(0, 15);
|
|
64
|
-
const reportPath = join(
|
|
64
|
+
const reportPath = join(projectDataDir(projectId), 'research', `audit-full-${ts}.md`);
|
|
65
65
|
const report = {
|
|
66
66
|
generatedAt: new Date().toISOString(),
|
|
67
67
|
totalSpecs: allSpecs.length,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AuditReport, DriftReviewPendingEntry } from '../../types/spec-audit.js';
|
|
2
2
|
export declare function buildMarkdownReport(report: AuditReport): string;
|
|
3
|
-
export declare function writeAuditReport(report: AuditReport,
|
|
3
|
+
export declare function writeAuditReport(report: AuditReport, _projectRoot: string): Promise<void>;
|
|
4
4
|
export declare function appendDriftReviewToPending(entry: DriftReviewPendingEntry, projectRoot: string): Promise<void>;
|
|
5
5
|
//# sourceMappingURL=report-writer.d.ts.map
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// engine/spec-audit/report-writer.ts — SPEC-744
|
|
2
|
-
// Generates the prioritised markdown drift report and appends pending
|
|
2
|
+
// Generates the prioritised markdown drift report and appends pending review entries.
|
|
3
3
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
4
4
|
import { join, dirname } from 'node:path';
|
|
5
5
|
import { existsSync } from 'node:fs';
|
|
6
6
|
import { atomicWriteFile } from '../safety/atomic-write-file.js';
|
|
7
|
+
import { hashProjectPath, projectDataDir } from '../../storage/base-store.js';
|
|
7
8
|
// ---------------------------------------------------------------------------
|
|
8
9
|
// Markdown report
|
|
9
10
|
// ---------------------------------------------------------------------------
|
|
@@ -57,8 +58,8 @@ export function buildMarkdownReport(report) {
|
|
|
57
58
|
// ---------------------------------------------------------------------------
|
|
58
59
|
// Write report atomically
|
|
59
60
|
// ---------------------------------------------------------------------------
|
|
60
|
-
export async function writeAuditReport(report,
|
|
61
|
-
const dir =
|
|
61
|
+
export async function writeAuditReport(report, _projectRoot) {
|
|
62
|
+
const dir = dirname(report.reportPath);
|
|
62
63
|
await mkdir(dir, { recursive: true });
|
|
63
64
|
const content = buildMarkdownReport(report);
|
|
64
65
|
await atomicWriteFile(report.reportPath, content);
|
|
@@ -67,7 +68,7 @@ export async function writeAuditReport(report, projectRoot) {
|
|
|
67
68
|
// Append to pending.json
|
|
68
69
|
// ---------------------------------------------------------------------------
|
|
69
70
|
export async function appendDriftReviewToPending(entry, projectRoot) {
|
|
70
|
-
const pendingPath = join(projectRoot, '
|
|
71
|
+
const pendingPath = join(projectDataDir(hashProjectPath(projectRoot)), 'pending.json');
|
|
71
72
|
let existing = [];
|
|
72
73
|
if (existsSync(pendingPath)) {
|
|
73
74
|
try {
|
|
@@ -1,15 +1,16 @@
|
|
|
1
|
+
import type { EnglishOnlyArtifactKind, EnglishOnlyValidationResult } from '../../types/spec-language.js';
|
|
1
2
|
/**
|
|
2
|
-
* Validate that persisted
|
|
3
|
+
* Validate that a Planu-owned persisted artifact is authored in English.
|
|
3
4
|
*
|
|
4
5
|
* The detector is intentionally conservative: it ignores code fences, inline code,
|
|
5
6
|
* file paths, markdown headings, and BDD keywords before looking for common
|
|
6
7
|
* Spanish/Portuguese function words. It should block obvious non-English prose
|
|
7
8
|
* without punishing short technical descriptions.
|
|
8
9
|
*/
|
|
9
|
-
export declare function
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
export declare function validateEnglishOnlyArtifactText(text: string, kind?: EnglishOnlyArtifactKind): EnglishOnlyValidationResult;
|
|
11
|
+
/**
|
|
12
|
+
* Backward-compatible spec-specific entrypoint used by create_spec/update_status gates.
|
|
13
|
+
*/
|
|
14
|
+
export declare function validateEnglishOnlySpecText(text: string): EnglishOnlyValidationResult;
|
|
15
|
+
export declare function assertEnglishOnlyArtifactText(text: string, kind: EnglishOnlyArtifactKind): void;
|
|
15
16
|
//# sourceMappingURL=english-only.d.ts.map
|
|
@@ -79,14 +79,14 @@ const NON_ENGLISH_SIGNATURES = {
|
|
|
79
79
|
]),
|
|
80
80
|
};
|
|
81
81
|
/**
|
|
82
|
-
* Validate that persisted
|
|
82
|
+
* Validate that a Planu-owned persisted artifact is authored in English.
|
|
83
83
|
*
|
|
84
84
|
* The detector is intentionally conservative: it ignores code fences, inline code,
|
|
85
85
|
* file paths, markdown headings, and BDD keywords before looking for common
|
|
86
86
|
* Spanish/Portuguese function words. It should block obvious non-English prose
|
|
87
87
|
* without punishing short technical descriptions.
|
|
88
88
|
*/
|
|
89
|
-
export function
|
|
89
|
+
export function validateEnglishOnlyArtifactText(text, kind = 'spec') {
|
|
90
90
|
const tokens = tokenizeProse(text);
|
|
91
91
|
if (tokens.length < MIN_PROSE_TOKENS) {
|
|
92
92
|
return { ok: true, detectedLanguage: 'unknown', signals: [] };
|
|
@@ -109,10 +109,34 @@ export function validateEnglishOnlySpecText(text) {
|
|
|
109
109
|
ok: false,
|
|
110
110
|
detectedLanguage: best.lang,
|
|
111
111
|
signals,
|
|
112
|
-
reason:
|
|
112
|
+
reason: `${artifactLabel(kind)} must be written in English. Detected ${languageName(best.lang)} prose ` +
|
|
113
113
|
`signals: ${signals.join(', ')}.`,
|
|
114
114
|
};
|
|
115
115
|
}
|
|
116
|
+
/**
|
|
117
|
+
* Backward-compatible spec-specific entrypoint used by create_spec/update_status gates.
|
|
118
|
+
*/
|
|
119
|
+
export function validateEnglishOnlySpecText(text) {
|
|
120
|
+
return validateEnglishOnlyArtifactText(text, 'spec');
|
|
121
|
+
}
|
|
122
|
+
export function assertEnglishOnlyArtifactText(text, kind) {
|
|
123
|
+
const validation = validateEnglishOnlyArtifactText(text, kind);
|
|
124
|
+
if (!validation.ok) {
|
|
125
|
+
throw new Error(validation.reason ?? `${artifactLabel(kind)} must be written in English.`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function artifactLabel(kind) {
|
|
129
|
+
switch (kind) {
|
|
130
|
+
case 'spec':
|
|
131
|
+
return 'Spec documents';
|
|
132
|
+
case 'skill':
|
|
133
|
+
return 'Skill artifacts';
|
|
134
|
+
case 'agent':
|
|
135
|
+
return 'Agent instructions';
|
|
136
|
+
case 'rule':
|
|
137
|
+
return 'Rule artifacts';
|
|
138
|
+
}
|
|
139
|
+
}
|
|
116
140
|
function tokenizeProse(text) {
|
|
117
141
|
const withoutCode = text
|
|
118
142
|
.replace(/```[\s\S]*?```/g, ' ')
|
|
@@ -13,4 +13,5 @@ export { findLegacyMultiFileSpecs } from './find-legacy-multifile-specs.js';
|
|
|
13
13
|
export { foldTechnicalIntoSpec } from './fold-technical.js';
|
|
14
14
|
export { foldProgressIntoSpec, isBoilerplateProgress } from './fold-progress.js';
|
|
15
15
|
export { runSsrBackMigration } from './ssr-back-migration.js';
|
|
16
|
+
export { PLANU_CANONICAL_POLICY, runStrictPlanuCleanup, validateStrictPlanuLayout, } from './strict-planu-cleanup.js';
|
|
16
17
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -26,4 +26,5 @@ export { findLegacyMultiFileSpecs } from './find-legacy-multifile-specs.js';
|
|
|
26
26
|
export { foldTechnicalIntoSpec } from './fold-technical.js';
|
|
27
27
|
export { foldProgressIntoSpec, isBoilerplateProgress } from './fold-progress.js';
|
|
28
28
|
export { runSsrBackMigration } from './ssr-back-migration.js';
|
|
29
|
+
export { PLANU_CANONICAL_POLICY, runStrictPlanuCleanup, validateStrictPlanuLayout, } from './strict-planu-cleanup.js';
|
|
29
30
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { PlanuCanonicalPathPolicy } from '../../types/index.js';
|
|
2
|
+
export declare const PLANU_CANONICAL_POLICY: PlanuCanonicalPathPolicy;
|
|
3
|
+
export declare function isCanonicalPlanuRootFile(name: string): boolean;
|
|
4
|
+
export declare function isCanonicalPlanuRootDir(name: string): boolean;
|
|
5
|
+
export declare function isCanonicalSpecFile(name: string): boolean;
|
|
6
|
+
export declare function mustMergeBeforeDeleteSpecFile(name: string): boolean;
|
|
7
|
+
export declare function isCanonicalReleaseFile(relativeToPlanu: string): boolean;
|
|
8
|
+
export declare function canonicalContractText(): string;
|
|
9
|
+
//# sourceMappingURL=planu-canonical-policy.d.ts.map
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// engine/spec-migrator/planu-canonical-policy.ts — SPEC-1017
|
|
2
|
+
// Single source of truth for the strict Planu managed directory contract.
|
|
3
|
+
export const PLANU_CANONICAL_POLICY = {
|
|
4
|
+
canonicalRootFiles: ['conventions.json', 'context.md', 'session-context.md', 'session.json'],
|
|
5
|
+
canonicalRootDirs: ['releases', 'specs'],
|
|
6
|
+
canonicalSpecFiles: ['spec.md'],
|
|
7
|
+
generatedRuntimePatterns: [
|
|
8
|
+
'planu/index.html',
|
|
9
|
+
'planu/roadmap.html',
|
|
10
|
+
'planu/status.json',
|
|
11
|
+
'planu/CHANGELOG.md',
|
|
12
|
+
'planu/.housekeeping-history.jsonl',
|
|
13
|
+
'planu/audits/',
|
|
14
|
+
'planu/handoffs/',
|
|
15
|
+
'planu/data/',
|
|
16
|
+
'planu/state/',
|
|
17
|
+
'planu/.locks/',
|
|
18
|
+
'planu/specs/data/',
|
|
19
|
+
'planu/specs/*/technical-report.html',
|
|
20
|
+
'planu/specs/*/reference/',
|
|
21
|
+
'planu/specs/*/test-stubs.ts',
|
|
22
|
+
'planu/specs/*/.analysis.json',
|
|
23
|
+
'planu/specs/*/prompt.md',
|
|
24
|
+
'planu/specs/*/implementation-brief.md',
|
|
25
|
+
'planu/specs/*/risk-register.md',
|
|
26
|
+
],
|
|
27
|
+
legacyMergeBeforeDeleteFiles: ['technical.md', 'plan.md', 'PLAN.md', 'progress.md'],
|
|
28
|
+
};
|
|
29
|
+
const ROOT_FILE_SET = new Set(PLANU_CANONICAL_POLICY.canonicalRootFiles);
|
|
30
|
+
const ROOT_DIR_SET = new Set(PLANU_CANONICAL_POLICY.canonicalRootDirs);
|
|
31
|
+
const SPEC_FILE_SET = new Set(PLANU_CANONICAL_POLICY.canonicalSpecFiles);
|
|
32
|
+
const LEGACY_MERGE_SET = new Set(PLANU_CANONICAL_POLICY.legacyMergeBeforeDeleteFiles);
|
|
33
|
+
export function isCanonicalPlanuRootFile(name) {
|
|
34
|
+
return ROOT_FILE_SET.has(name);
|
|
35
|
+
}
|
|
36
|
+
export function isCanonicalPlanuRootDir(name) {
|
|
37
|
+
return ROOT_DIR_SET.has(name);
|
|
38
|
+
}
|
|
39
|
+
export function isCanonicalSpecFile(name) {
|
|
40
|
+
return SPEC_FILE_SET.has(name);
|
|
41
|
+
}
|
|
42
|
+
export function mustMergeBeforeDeleteSpecFile(name) {
|
|
43
|
+
return LEGACY_MERGE_SET.has(name);
|
|
44
|
+
}
|
|
45
|
+
export function isCanonicalReleaseFile(relativeToPlanu) {
|
|
46
|
+
return relativeToPlanu === 'releases/pending.json';
|
|
47
|
+
}
|
|
48
|
+
export function canonicalContractText() {
|
|
49
|
+
return [
|
|
50
|
+
'planu/',
|
|
51
|
+
' conventions.json',
|
|
52
|
+
' context.md',
|
|
53
|
+
' session-context.md',
|
|
54
|
+
' session.json',
|
|
55
|
+
' releases/',
|
|
56
|
+
' pending.json',
|
|
57
|
+
' specs/',
|
|
58
|
+
' SPEC-XXX-slug/',
|
|
59
|
+
' spec.md',
|
|
60
|
+
].join('\n');
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=planu-canonical-policy.js.map
|
|
@@ -1,101 +1,25 @@
|
|
|
1
|
-
// engine/spec-migrator/planu-root-cleaner.ts — SPEC-
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
import { execFileSync } from 'node:child_process';
|
|
5
|
-
import { join, relative } from 'node:path';
|
|
6
|
-
import { safeUnlink } from './git-aware-fs.js';
|
|
7
|
-
/** Files allowed in planu/ root. Everything else gets deleted. */
|
|
8
|
-
const CANONICAL_ROOT_FILES = new Set([
|
|
9
|
-
'status.json',
|
|
10
|
-
'conventions.json',
|
|
11
|
-
'index.html',
|
|
12
|
-
'roadmap.html',
|
|
13
|
-
]);
|
|
14
|
-
/** Directories allowed in planu/ root. */
|
|
15
|
-
const CANONICAL_ROOT_DIRS = new Set(['specs']);
|
|
16
|
-
/** Files allowed inside each planu/specs/SPEC-XXX/ directory. */
|
|
17
|
-
const CANONICAL_SPEC_FILES = new Set(['spec.md']);
|
|
18
|
-
/** Remove a file from git tracking (best-effort, silent if not a git repo or file not tracked). */
|
|
19
|
-
function gitRmCached(absolutePath, cwd) {
|
|
20
|
-
try {
|
|
21
|
-
const rel = relative(cwd, absolutePath);
|
|
22
|
-
execFileSync('git', ['rm', '--cached', '--quiet', '--force', rel], {
|
|
23
|
-
cwd,
|
|
24
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
25
|
-
timeout: 3_000,
|
|
26
|
-
});
|
|
27
|
-
}
|
|
28
|
-
catch {
|
|
29
|
-
// Not a git repo, file not tracked, or git not available — ignore
|
|
30
|
-
}
|
|
31
|
-
}
|
|
1
|
+
// engine/spec-migrator/planu-root-cleaner.ts — SPEC-1017 strict wrapper
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
import { runStrictPlanuCleanup } from './strict-planu-cleanup.js';
|
|
32
4
|
/** Remove all non-canonical files from planu/ root and per-spec directories. */
|
|
33
5
|
export async function cleanPlanuRoot(planuDir, projectPath) {
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
entries = await readdir(planuDir);
|
|
43
|
-
}
|
|
44
|
-
catch {
|
|
45
|
-
return result; // Dir doesn't exist — nothing to clean
|
|
46
|
-
}
|
|
47
|
-
for (const entry of entries) {
|
|
48
|
-
const fullPath = join(planuDir, entry);
|
|
49
|
-
try {
|
|
50
|
-
const info = await stat(fullPath);
|
|
51
|
-
if (info.isDirectory()) {
|
|
52
|
-
if (!CANONICAL_ROOT_DIRS.has(entry)) {
|
|
53
|
-
// Non-canonical directory — skip (don't delete dirs recursively for safety)
|
|
54
|
-
continue;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
else if (!CANONICAL_ROOT_FILES.has(entry)) {
|
|
58
|
-
gitRmCached(fullPath, planuDir);
|
|
59
|
-
await safeUnlink(projectPath ?? planuDir, fullPath);
|
|
60
|
-
result.deletedRootFiles.push(entry);
|
|
61
|
-
}
|
|
6
|
+
const root = projectPath ?? dirname(planuDir);
|
|
7
|
+
const strict = await runStrictPlanuCleanup(root);
|
|
8
|
+
const deletedRootFiles = [];
|
|
9
|
+
const deletedSpecFiles = [];
|
|
10
|
+
for (const path of [...strict.deleted, ...strict.merged]) {
|
|
11
|
+
const normalized = path.replace(/^planu\//, '');
|
|
12
|
+
if (normalized.startsWith('specs/')) {
|
|
13
|
+
deletedSpecFiles.push(normalized.replace(/^specs\//, ''));
|
|
62
14
|
}
|
|
63
|
-
|
|
64
|
-
|
|
15
|
+
else {
|
|
16
|
+
deletedRootFiles.push(normalized);
|
|
65
17
|
}
|
|
66
18
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
catch {
|
|
74
|
-
result.totalDeleted = result.deletedRootFiles.length;
|
|
75
|
-
return result;
|
|
76
|
-
}
|
|
77
|
-
for (const specDir of specDirs) {
|
|
78
|
-
const specPath = join(specsDir, specDir);
|
|
79
|
-
try {
|
|
80
|
-
const info = await stat(specPath);
|
|
81
|
-
if (!info.isDirectory()) {
|
|
82
|
-
continue;
|
|
83
|
-
}
|
|
84
|
-
const files = await readdir(specPath);
|
|
85
|
-
for (const file of files) {
|
|
86
|
-
if (!CANONICAL_SPEC_FILES.has(file)) {
|
|
87
|
-
const filePath = join(specPath, file);
|
|
88
|
-
gitRmCached(filePath, planuDir);
|
|
89
|
-
await safeUnlink(projectPath ?? planuDir, filePath);
|
|
90
|
-
result.deletedSpecFiles.push(`${specDir}/${file}`);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
catch {
|
|
95
|
-
// Spec dir issue — skip
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
result.totalDeleted = result.deletedRootFiles.length + result.deletedSpecFiles.length;
|
|
99
|
-
return result;
|
|
19
|
+
return {
|
|
20
|
+
deletedRootFiles,
|
|
21
|
+
deletedSpecFiles,
|
|
22
|
+
totalDeleted: strict.deleted.length + strict.merged.length,
|
|
23
|
+
};
|
|
100
24
|
}
|
|
101
25
|
//# sourceMappingURL=planu-root-cleaner.js.map
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { StrictPlanuCleanupResult, StrictPlanuValidationResult } from '../../types/index.js';
|
|
2
|
+
import { PLANU_CANONICAL_POLICY } from './planu-canonical-policy.js';
|
|
3
|
+
export declare function runStrictPlanuCleanup(projectPath: string): Promise<StrictPlanuCleanupResult>;
|
|
4
|
+
export declare function validateStrictPlanuLayout(projectPath: string): Promise<StrictPlanuValidationResult>;
|
|
5
|
+
export { PLANU_CANONICAL_POLICY };
|
|
6
|
+
//# sourceMappingURL=strict-planu-cleanup.d.ts.map
|