@planu/cli 4.0.0 → 4.1.1

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.
Files changed (35) hide show
  1. package/CHANGELOG.md +22 -1
  2. package/dist/engine/ai-integration/agents-md/generator.js +4 -1
  3. package/dist/engine/ai-integration/cline/clinerules-generator.js +7 -2
  4. package/dist/engine/ai-integration/codex/agents-md-generator.js +2 -0
  5. package/dist/engine/ai-integration/cursor/cursorrules-generator.js +7 -2
  6. package/dist/engine/ai-integration/windsurf/windsurfrules-generator.js +7 -2
  7. package/dist/engine/hooks/git-hook-generator.js +31 -0
  8. package/dist/engine/marketplace-fetcher/anthropic-source.js +2 -0
  9. package/dist/engine/opencode/config-scaffold.js +4 -0
  10. package/dist/engine/rules-generator/index.js +2 -0
  11. package/dist/engine/rules-reconciler.js +2 -0
  12. package/dist/engine/skill-bootstrap/skill-writer.js +2 -0
  13. package/dist/engine/skill-generation/multi-agent-writer.js +2 -0
  14. package/dist/engine/spec-language/english-only.d.ts +8 -7
  15. package/dist/engine/spec-language/english-only.js +27 -3
  16. package/dist/engine/universal-rules/host-writer.js +8 -2
  17. package/dist/engine/universal-rules/rules/planu-english-specs.js +9 -5
  18. package/dist/hosts/claude-code/ux/skills-writer.js +2 -0
  19. package/dist/hosts/codex/config-scaffold.js +5 -0
  20. package/dist/hosts/gemini/config-scaffold.js +4 -0
  21. package/dist/tools/create-skill.js +21 -0
  22. package/dist/tools/git/hook-ops.js +30 -0
  23. package/dist/tools/init-project/agents-md-writer.js +2 -0
  24. package/dist/tools/init-project/conventions-writer.js +2 -0
  25. package/dist/tools/init-project/find-skills-writer.js +2 -0
  26. package/dist/tools/init-project/helpers.js +5 -0
  27. package/dist/tools/init-project/per-client-files-writer.js +2 -0
  28. package/dist/tools/init-project/planu-workflow-generator.js +2 -0
  29. package/dist/tools/init-project/rules-generator.js +7 -1
  30. package/dist/tools/init-project/rules-writer.js +3 -0
  31. package/dist/tools/init-project/skills-multi-teammate-review-writer.js +2 -0
  32. package/dist/tools/init-project/skills-writer.js +2 -0
  33. package/dist/types/spec-language.d.ts +8 -0
  34. package/dist/types/spec-language.js +2 -0
  35. package/package.json +21 -20
package/CHANGELOG.md CHANGED
@@ -1,3 +1,24 @@
1
+ ## [4.1.1] - 2026-05-21
2
+
3
+ ### Features
4
+ - Enforce dependency freshness as a blocking pre-push gate for lockfile drift, high/critical vulnerabilities, and outdated direct dependencies.
5
+ - Add the same pnpm dependency freshness guard to Planu-generated pre-push hooks.
6
+
7
+ ### Tests
8
+ - Add coverage for the dependency freshness script and generated hook contents.
9
+
10
+
11
+ ## [4.1.0] - 2026-05-21
12
+
13
+ ### Features
14
+ - Enforce English-only persisted Planu artifacts across specs, skills, agent instructions, and rules.
15
+ - Gate host-aware init scaffolding for `AGENTS.md`, `CLAUDE.md`, Cursor, Windsurf, Cline, Gemini, Codex, and OpenCode generated AI docs.
16
+ - Keep user-authored host file content intact while validating only Planu-owned generated blocks.
17
+
18
+ ### Tests
19
+ - Add coverage for skill, rule, and agent instruction language gates across core writers and host generators.
20
+
21
+
1
22
  ## [4.0.0] - 2026-05-20
2
23
 
3
24
  **Tarball SHA-256:** `8c00d74f48ed5614197000a967b103cc17653150aadf876fcfd18d0174263017`
@@ -3792,4 +3813,4 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) · Versioning:
3792
3813
  - Mermaid diagram generation (architecture, sequence, state machine, ER, data flow)
3793
3814
  - Multi-language i18n (EN/ES/PT) for generated specs
3794
3815
  - Clean Architecture (hexagonal) — engine, tools, storage, types layers
3795
- - 10,857 tests with ≥95% coverage
3816
+ - 10,857 tests with ≥95% coverage
@@ -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
- return parts.join('\n');
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
- return lines.join('\n');
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
- return header + buildPlanuSection(options);
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
  /**
@@ -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
- return lines.join('\n');
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
- return header + buildPlanuSection(options);
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');
@@ -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
- return lines.join('\n');
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
- return header + buildPlanuSection(options);
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');
@@ -66,6 +66,37 @@ if command -v planu >/dev/null 2>&1; then
66
66
  planu detect-drift --project-id "${projectId}" --mode quick 2>/dev/null || true
67
67
  fi
68
68
 
69
+ if command -v pnpm >/dev/null 2>&1 && [ -f "package.json" ] && [ -f "pnpm-lock.yaml" ]; then
70
+ echo "[Planu] Checking dependency freshness..."
71
+ pnpm install --frozen-lockfile --ignore-scripts >/dev/null || {
72
+ echo "ERROR: package.json and pnpm-lock.yaml are not synchronized."
73
+ echo "Run: pnpm install --lockfile-only --ignore-scripts"
74
+ exit 1
75
+ }
76
+ pnpm audit --audit-level=high || {
77
+ echo "ERROR: High or critical dependency vulnerabilities found."
78
+ echo "Run: pnpm audit and update the affected packages before pushing."
79
+ exit 1
80
+ }
81
+ OUTDATED_JSON="$(mktemp)"
82
+ OUTDATED_ERR="$(mktemp)"
83
+ set +e
84
+ pnpm outdated --format=json >"\${OUTDATED_JSON}" 2>"\${OUTDATED_ERR}"
85
+ OUTDATED_STATUS=$?
86
+ set -e
87
+ if [ "\${OUTDATED_STATUS}" -gt 1 ]; then
88
+ echo "ERROR: Failed to query dependency freshness from the registry."
89
+ cat "\${OUTDATED_ERR}"
90
+ rm -f "\${OUTDATED_JSON}" "\${OUTDATED_ERR}"
91
+ exit 1
92
+ fi
93
+ node -e "const fs=require('fs');const raw=fs.readFileSync(process.argv[1],'utf8').trim();const data=raw?JSON.parse(raw):{};const count=Array.isArray(data)?data.length:Object.keys(data).length;if(count>0){console.error('ERROR: Outdated dependencies found. Run: bash scripts/check-updates.sh --apply, pnpm update, or update intentionally before pushing.');process.exit(1)}" "\${OUTDATED_JSON}" || {
94
+ rm -f "\${OUTDATED_JSON}" "\${OUTDATED_ERR}"
95
+ exit 1
96
+ }
97
+ rm -f "\${OUTDATED_JSON}" "\${OUTDATED_ERR}"
98
+ fi
99
+
69
100
  echo "[Planu] Pre-push checks complete."
70
101
  `;
71
102
  }
@@ -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)');
@@ -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
  }
@@ -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) => {
@@ -1,15 +1,16 @@
1
+ import type { EnglishOnlyArtifactKind, EnglishOnlyValidationResult } from '../../types/spec-language.js';
1
2
  /**
2
- * Validate that persisted spec documentation is authored in English.
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 validateEnglishOnlySpecText(text: string): {
10
- ok: boolean;
11
- detectedLanguage: 'en' | 'es' | 'pt' | 'unknown';
12
- reason?: string;
13
- signals: string[];
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 spec documentation is authored in English.
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 validateEnglishOnlySpecText(text) {
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: `Spec documents must be written in English. Detected ${languageName(best.lang)} prose ` +
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, ' ')
@@ -2,6 +2,7 @@
2
2
  // Write a UniversalRule to disk using the appropriate host strategy.
3
3
  import { readFile, mkdir, writeFile } from 'node:fs/promises';
4
4
  import { join } from 'node:path';
5
+ import { assertEnglishOnlyArtifactText } from '../spec-language/english-only.js';
5
6
  // ---------------------------------------------------------------------------
6
7
  // Claude Code writer
7
8
  // ---------------------------------------------------------------------------
@@ -14,6 +15,7 @@ async function writeForClaudeCode(rule, projectPath) {
14
15
  await mkdir(rulesDir, { recursive: true });
15
16
  const filePath = join(rulesDir, `${rule.id}.md`);
16
17
  const content = rule.buildContent('claude-code');
18
+ assertEnglishOnlyArtifactText(`${rule.name}\n\n${rule.description}\n\n${content}`, 'rule');
17
19
  await writeFile(filePath, content, 'utf-8');
18
20
  return filePath;
19
21
  }
@@ -39,7 +41,9 @@ async function writeForCodex(rule, projectPath) {
39
41
  }
40
42
  const open = OPEN_MARKER(rule.id);
41
43
  const close = CLOSE_MARKER(rule.id);
42
- const block = `${open}\n${rule.buildContent('codex')}\n${close}`;
44
+ const content = rule.buildContent('codex');
45
+ assertEnglishOnlyArtifactText(`${rule.name}\n\n${rule.description}\n\n${content}`, 'rule');
46
+ const block = `${open}\n${content}\n${close}`;
43
47
  // Replace existing block or append
44
48
  const openIdx = existing.indexOf(open);
45
49
  const closeIdx = existing.indexOf(close);
@@ -74,7 +78,9 @@ async function writeForGemini(rule, projectPath) {
74
78
  }
75
79
  const open = OPEN_MARKER(rule.id);
76
80
  const close = CLOSE_MARKER(rule.id);
77
- const block = `${open}\n${rule.buildContent('gemini')}\n${close}`;
81
+ const content = rule.buildContent('gemini');
82
+ assertEnglishOnlyArtifactText(`${rule.name}\n\n${rule.description}\n\n${content}`, 'rule');
83
+ const block = `${open}\n${content}\n${close}`;
78
84
  const openIdx = existing.indexOf(open);
79
85
  const closeIdx = existing.indexOf(close);
80
86
  let updated;
@@ -1,31 +1,35 @@
1
1
  // engine/universal-rules/rules/planu-english-specs.ts — Universal rule: specs are written in English
2
2
  function buildBody() {
3
- return `# Planu Specs Must Be Written in English
3
+ return `# Planu Artifacts Must Be Written in English
4
4
 
5
5
  Auto-generated by \`init_project\`. Do not edit manually.
6
6
 
7
7
  ## Rule
8
8
 
9
- All generated spec content is written in English, regardless of the user's conversation language:
9
+ All generated Planu-owned artifacts are written in English, regardless of the user's conversation language:
10
10
 
11
11
  - \`spec.md\`
12
+ - Planu skills, including \`SKILL.md\` and host-adapted skill blocks
13
+ - Planu agent instructions, including \`AGENTS.md\`, \`CLAUDE.md\`, \`GEMINI.md\`, and host-specific sections
14
+ - Planu rules, including \`.claude/rules/*.md\`, Codex rule blocks, and Gemini conventions
12
15
  - inline \`## Technical\`, \`## Files\`, and \`## Progress\` sections
13
16
  - acceptance criteria, implementation notes, validation notes, and reconciliation notes
14
17
 
15
- User-facing chat may use the user's preferred language. The spec contract itself stays in English so every agent, tool, and CI gate can parse it consistently.
18
+ User-facing chat may use the user's preferred language. The persisted Planu contract stays in English so every agent, tool, and CI gate can parse it consistently.
16
19
 
17
20
  ## Hard Blocks
18
21
 
19
22
  - Do not create mixed-language acceptance criteria.
20
23
  - Do not translate BDD keywords.
24
+ - Do not generate Planu-owned skills, agents, or rules in Spanish, Portuguese, or mixed language.
21
25
  - Do not create standalone \`technical.md\`, \`progress.md\`, \`HU.md\`, \`FICHA-TECNICA.md\`, or \`PROGRESS.md\`.
22
26
  - Do not approve a spec that contains unresolved placeholders such as \`to be determined\`, \`TBD\`, \`TODO\`, or equivalent filler.
23
27
  `;
24
28
  }
25
29
  export const planuEnglishSpecsRule = {
26
30
  id: 'planu-english-specs',
27
- name: 'Planu English Specs',
28
- description: 'Requires Planu spec artifacts to be written in English.',
31
+ name: 'Planu English Artifacts',
32
+ description: 'Requires Planu spec, skill, agent, and rule artifacts to be written in English.',
29
33
  category: 'quality',
30
34
  applicableHosts: ['all'],
31
35
  defaultEnabled: true,
@@ -4,6 +4,7 @@
4
4
  import { readFile, writeFile, mkdir, access } from 'node:fs/promises';
5
5
  import { join } from 'node:path';
6
6
  import { fileURLToPath } from 'node:url';
7
+ import { assertEnglishOnlyArtifactText } from '../../../engine/spec-language/english-only.js';
7
8
  const SKILLS_DIR = '.claude/skills';
8
9
  /** Canonical skill slugs managed by SPEC-588. */
9
10
  const SKILL_SLUGS = [
@@ -68,6 +69,7 @@ async function writeSkillFile(projectPath, slug, content) {
68
69
  if (existing === content) {
69
70
  return { path: dest, created: false };
70
71
  }
72
+ assertEnglishOnlyArtifactText(content, 'skill');
71
73
  await writeFile(dest, content, 'utf-8');
72
74
  return { path: dest, created: existing === null };
73
75
  }
@@ -12,6 +12,7 @@ import { mkdir, writeFile, readFile, access } from 'node:fs/promises';
12
12
  import { join } from 'node:path';
13
13
  import { detectCodexWorkspace } from './workspace-scope.js';
14
14
  import { buildRulesForHost } from '../../engine/host-rules-templates/index.js';
15
+ import { assertEnglishOnlyArtifactText } from '../../engine/spec-language/english-only.js';
15
16
  const CODEX_CONFIG_TOML = `# .openai/config.toml — Planu Codex workspace configuration
16
17
  # Auto-generated by Planu init_project. Safe to customize.
17
18
 
@@ -79,6 +80,9 @@ async function writeIfMissing(filePath, content) {
79
80
  // File exists — skip (idempotent)
80
81
  }
81
82
  catch {
83
+ if (filePath.endsWith('AGENTS.md')) {
84
+ assertEnglishOnlyArtifactText(content, 'agent');
85
+ }
82
86
  await writeFile(filePath, content, 'utf-8');
83
87
  }
84
88
  }
@@ -91,6 +95,7 @@ const PLANU_RULES_START_REGEX = /<!-- planu:rules:start[^>]* -->/;
91
95
  */
92
96
  async function injectRulesBlock(filePath, version) {
93
97
  const block = buildRulesForHost('codex', version).rules;
98
+ assertEnglishOnlyArtifactText(block, 'rule');
94
99
  let existing;
95
100
  try {
96
101
  existing = await readFile(filePath, 'utf-8');
@@ -5,6 +5,7 @@
5
5
  import { writeFile, readFile, mkdir, access } from 'node:fs/promises';
6
6
  import { join } from 'node:path';
7
7
  import { buildRulesForHost } from '../../engine/host-rules-templates/index.js';
8
+ import { assertEnglishOnlyArtifactText } from '../../engine/spec-language/english-only.js';
8
9
  // ---------------------------------------------------------------------------
9
10
  // File content generators
10
11
  // ---------------------------------------------------------------------------
@@ -144,6 +145,8 @@ export async function scaffoldGeminiConfig(projectPath, planuVersion = '1.96.0')
144
145
  // Ensure parent directory exists
145
146
  const parentDir = join(projectPath, relPath.split('/').slice(0, -1).join('/'));
146
147
  await mkdir(parentDir, { recursive: true });
148
+ const artifactKind = relPath.includes('/skills/') ? 'skill' : 'agent';
149
+ assertEnglishOnlyArtifactText(content, artifactKind);
147
150
  await writeFile(fullPath, content, 'utf-8');
148
151
  filesCreated.push(relPath);
149
152
  }
@@ -166,6 +169,7 @@ const PLANU_RULES_START_REGEX = /<!-- planu:rules:start[^>]* -->/;
166
169
  */
167
170
  async function injectRulesBlock(filePath, version) {
168
171
  const block = buildRulesForHost('gemini', version).rules;
172
+ assertEnglishOnlyArtifactText(block, 'rule');
169
173
  let existing;
170
174
  try {
171
175
  existing = await readFile(filePath, 'utf-8');
@@ -3,6 +3,7 @@
3
3
  import { readFile, mkdir, writeFile } from 'node:fs/promises';
4
4
  import { join } from 'node:path';
5
5
  import { detectHost } from '../engine/host-detection/detect-host.js';
6
+ import { validateEnglishOnlyArtifactText } from '../engine/spec-language/english-only.js';
6
7
  import { hashContent } from '../engine/universal-rules/user-edit-detector.js';
7
8
  import { hashProjectPath } from '../storage/base-store.js';
8
9
  import { appendAutopilotLogEntry } from '../storage/autopilot-log-store.js';
@@ -96,6 +97,26 @@ export async function handleCreateSkill(input) {
96
97
  const { projectPath, name, content, overwriteExisting } = input;
97
98
  const description = input.description ?? '';
98
99
  const host = resolveHost(input);
100
+ const languageValidation = validateEnglishOnlyArtifactText(`${name}\n\n${description}\n\n${content}`, 'skill');
101
+ if (!languageValidation.ok) {
102
+ return {
103
+ content: [
104
+ {
105
+ type: 'text',
106
+ text: `English-only skill gate blocked create_skill.\n\n` +
107
+ `${languageValidation.reason ?? 'Non-English prose detected.'}\n\n` +
108
+ 'Rewrite the skill name, description, and content in English before creating it.',
109
+ },
110
+ ],
111
+ isError: true,
112
+ structuredContent: {
113
+ error: 'SKILL_LANGUAGE_GATE_BLOCKED',
114
+ detectedLanguage: languageValidation.detectedLanguage,
115
+ signals: languageValidation.signals,
116
+ fixHint: 'Rewrite skill artifacts in English.',
117
+ },
118
+ };
119
+ }
99
120
  let result;
100
121
  try {
101
122
  switch (host) {
@@ -165,6 +165,36 @@ function buildPrePushScript(protectedBranches, stalenessThreshold, baseBranch) {
165
165
  ' fi',
166
166
  'fi',
167
167
  '',
168
+ '# Dependency freshness gate for pnpm projects',
169
+ 'if command -v pnpm >/dev/null 2>&1 && [ -f "package.json" ] && [ -f "pnpm-lock.yaml" ]; then',
170
+ ' echo "[Planu] Checking dependency freshness..."',
171
+ ' pnpm install --frozen-lockfile --ignore-scripts >/dev/null || {',
172
+ ' echo "ERROR: package.json and pnpm-lock.yaml are not synchronized."',
173
+ ' echo "Run: pnpm install --lockfile-only --ignore-scripts"',
174
+ ' exit 1',
175
+ ' }',
176
+ ' pnpm audit --audit-level=high || {',
177
+ ' echo "ERROR: High or critical dependency vulnerabilities found."',
178
+ ' echo "Run: pnpm audit and update the affected packages before pushing."',
179
+ ' exit 1',
180
+ ' }',
181
+ ' OUTDATED_JSON=$(mktemp)',
182
+ ' OUTDATED_ERR=$(mktemp)',
183
+ ' pnpm outdated --format=json >"$OUTDATED_JSON" 2>"$OUTDATED_ERR"',
184
+ ' OUTDATED_STATUS=$?',
185
+ ' if [ "$OUTDATED_STATUS" -gt 1 ]; then',
186
+ ' echo "ERROR: Failed to query dependency freshness from the registry."',
187
+ ' cat "$OUTDATED_ERR"',
188
+ ' rm -f "$OUTDATED_JSON" "$OUTDATED_ERR"',
189
+ ' exit 1',
190
+ ' fi',
191
+ " node -e \"const fs=require('fs');const raw=fs.readFileSync(process.argv[1],'utf8').trim();const data=raw?JSON.parse(raw):{};const count=Array.isArray(data)?data.length:Object.keys(data).length;if(count>0){console.error('ERROR: Outdated dependencies found. Run: bash scripts/check-updates.sh --apply, pnpm update, or update intentionally before pushing.');process.exit(1)}\" \"$OUTDATED_JSON\" || {",
192
+ ' rm -f "$OUTDATED_JSON" "$OUTDATED_ERR"',
193
+ ' exit 1',
194
+ ' }',
195
+ ' rm -f "$OUTDATED_JSON" "$OUTDATED_ERR"',
196
+ 'fi',
197
+ '',
168
198
  'exit 0',
169
199
  ].join('\n');
170
200
  }
@@ -3,6 +3,7 @@
3
3
  // SPEC-972: Added host-aware tool filtering for Codex and other MCP hosts.
4
4
  import { writeFile, mkdir, access } from 'node:fs/promises';
5
5
  import { join } from 'node:path';
6
+ import { assertEnglishOnlyArtifactText } from '../../engine/spec-language/english-only.js';
6
7
  import { CORE_AGENTS_MD_TOOLS, loadHostRegistry, filterToolsByHost, formatToolsForAgentsMd, } from '../../engine/host-tool-filter.js';
7
8
  const AGENTS_MD_PATH = 'AGENTS.md';
8
9
  const CURSORRULES_PATH = '.cursorrules';
@@ -89,6 +90,7 @@ async function writeIfMissing(path, content) {
89
90
  return false;
90
91
  }
91
92
  catch {
93
+ assertEnglishOnlyArtifactText(content, 'agent');
92
94
  await writeFile(path, content, 'utf-8');
93
95
  return true;
94
96
  }
@@ -2,6 +2,7 @@
2
2
  // with project-specific conventions detected by init_project
3
3
  import { writeFile, mkdir, access } from 'node:fs/promises';
4
4
  import { join } from 'node:path';
5
+ import { assertEnglishOnlyArtifactText } from '../../engine/spec-language/english-only.js';
5
6
  const CONVENTIONS_MD_PATH = '.claude/rules/conventions.md';
6
7
  function buildConventionsContent(knowledge) {
7
8
  const stackItems = knowledge.stack;
@@ -44,6 +45,7 @@ export async function generateConventionsMdIfMissing(projectPath, knowledge) {
44
45
  const rulesDir = join(projectPath, '.claude/rules');
45
46
  await mkdir(rulesDir, { recursive: true });
46
47
  const content = buildConventionsContent(knowledge);
48
+ assertEnglishOnlyArtifactText(content, 'rule');
47
49
  await writeFile(conventionsPath, content, 'utf-8');
48
50
  return true;
49
51
  }
@@ -2,6 +2,7 @@
2
2
  // Single hardcoded entry point for skill discovery in generated config files
3
3
  import { writeFile, mkdir, access } from 'node:fs/promises';
4
4
  import { join } from 'node:path';
5
+ import { assertEnglishOnlyArtifactText } from '../../engine/spec-language/english-only.js';
5
6
  const FIND_SKILLS_PATH = '.claude/skills/find-skills.md';
6
7
  const FIND_SKILLS_CONTENT = `# /find-skills — Discover Available Workflows
7
8
 
@@ -42,6 +43,7 @@ export async function generateFindSkillsIfMissing(projectPath) {
42
43
  catch {
43
44
  const skillsDir = join(projectPath, '.claude', 'skills');
44
45
  await mkdir(skillsDir, { recursive: true });
46
+ assertEnglishOnlyArtifactText(FIND_SKILLS_CONTENT, 'skill');
45
47
  await writeFile(skillPath, FIND_SKILLS_CONTENT, 'utf-8');
46
48
  return true;
47
49
  }
@@ -1,4 +1,5 @@
1
1
  import { detectLegalFramework, detectThirdParties, buildDefaultPrivacyConfig, } from '../../engine/pii-detector.js';
2
+ import { assertEnglishOnlyArtifactText } from '../../engine/spec-language/english-only.js';
2
3
  import { readFile, writeFile, access, stat } from 'node:fs/promises';
3
4
  import { join } from 'node:path';
4
5
  import { glob } from 'glob';
@@ -283,6 +284,10 @@ export async function injectSddIntoClaude(projectPath, projectSections) {
283
284
  if (updated === content) {
284
285
  continue; // nothing changed for this file
285
286
  }
287
+ assertEnglishOnlyArtifactText(SDD_BLOCK, 'agent');
288
+ if (projectSections) {
289
+ assertEnglishOnlyArtifactText(projectSections, 'agent');
290
+ }
286
291
  await writeFile(filePath, updated, 'utf-8');
287
292
  anyModified = true;
288
293
  }
@@ -2,6 +2,7 @@
2
2
  import { writeFile, mkdir, access } from 'node:fs/promises';
3
3
  import { join } from 'node:path';
4
4
  import { detectAndCacheClient } from '../../engine/client-detection.js';
5
+ import { assertEnglishOnlyArtifactText } from '../../engine/spec-language/english-only.js';
5
6
  const CURSOR_MDC_CONTENT = `---
6
7
  description: Planu SDD workflow for Cursor
7
8
  globs: ["**/*"]
@@ -70,6 +71,7 @@ async function writeIfMissing(filePath, content) {
70
71
  }
71
72
  catch {
72
73
  await mkdir(join(filePath, '..'), { recursive: true });
74
+ assertEnglishOnlyArtifactText(content, filePath.includes('/skills/') ? 'skill' : 'agent');
73
75
  await writeFile(filePath, content, 'utf-8');
74
76
  return true;
75
77
  }
@@ -2,6 +2,7 @@
2
2
  // SPEC-263: Autonomous CLAUDE.md that makes Planu mandatory
3
3
  import { readFile, writeFile, mkdir } from 'node:fs/promises';
4
4
  import { dirname } from 'node:path';
5
+ import { assertEnglishOnlyArtifactText } from '../../engine/spec-language/english-only.js';
5
6
  const PLANU_WORKFLOW_MARKER = '<!-- planu-sdd-workflow -->';
6
7
  const PLANU_WORKFLOW_CONTENT = `<!-- planu-sdd-workflow -->
7
8
  ## Planu SDD Workflow (MANDATORY)
@@ -61,6 +62,7 @@ export async function injectPlanuSection(claudeMdPath, section) {
61
62
  await mkdir(dirname(claudeMdPath), { recursive: true });
62
63
  }
63
64
  const updated = injectOrReplacePlanuBlock(existing, section.content, section.marker);
65
+ assertEnglishOnlyArtifactText(section.content, 'agent');
64
66
  await writeFile(claudeMdPath, updated, 'utf-8');
65
67
  }
66
68
  /**
@@ -2,6 +2,7 @@
2
2
  // SPEC-263: Autonomous CLAUDE.md that makes Planu mandatory
3
3
  import { writeFile, mkdir, access } from 'node:fs/promises';
4
4
  import { join } from 'node:path';
5
+ import { assertEnglishOnlyArtifactText } from '../../engine/spec-language/english-only.js';
5
6
  const PLANU_WORKFLOW_RULES_PATH = '.claude/rules/planu-workflow.md';
6
7
  const PLANU_WORKFLOW_RULES_CONTENT = `# Planu SDD Workflow (MANDATORY)
7
8
 
@@ -141,7 +142,9 @@ export async function generateAgentTeamsRules(projectPath) {
141
142
  const rulesPath = join(projectPath, AGENT_TEAMS_RULES_PATH);
142
143
  const rulesDir = join(projectPath, '.claude/rules');
143
144
  await mkdir(rulesDir, { recursive: true });
144
- await writeFile(rulesPath, generateAgentTeamsRulesContent(), 'utf-8');
145
+ const content = generateAgentTeamsRulesContent();
146
+ assertEnglishOnlyArtifactText(content, 'rule');
147
+ await writeFile(rulesPath, content, 'utf-8');
145
148
  }
146
149
  /**
147
150
  * Write .claude/rules/agent-teams.md only if it does not exist yet.
@@ -166,6 +169,7 @@ export async function generateWorkflowRules(projectPath) {
166
169
  const rulesPath = join(projectPath, PLANU_WORKFLOW_RULES_PATH);
167
170
  const rulesDir = join(projectPath, '.claude/rules');
168
171
  await mkdir(rulesDir, { recursive: true });
172
+ assertEnglishOnlyArtifactText(PLANU_WORKFLOW_RULES_CONTENT, 'rule');
169
173
  await writeFile(rulesPath, PLANU_WORKFLOW_RULES_CONTENT, 'utf-8');
170
174
  }
171
175
  /**
@@ -227,6 +231,7 @@ export async function generateModeRulesIfMissing(projectPath) {
227
231
  catch {
228
232
  const rulesDir = join(projectPath, '.claude/rules');
229
233
  await mkdir(rulesDir, { recursive: true });
234
+ assertEnglishOnlyArtifactText(PLANU_MODES_RULES_CONTENT, 'rule');
230
235
  await writeFile(rulesPath, PLANU_MODES_RULES_CONTENT, 'utf-8');
231
236
  return true;
232
237
  }
@@ -254,6 +259,7 @@ export async function generateResponseStyleRulesIfMissing(projectPath) {
254
259
  catch {
255
260
  const rulesDir = join(projectPath, '.claude/rules');
256
261
  await mkdir(rulesDir, { recursive: true });
262
+ assertEnglishOnlyArtifactText(RESPONSE_STYLE_RULES_CONTENT, 'rule');
257
263
  await writeFile(rulesPath, RESPONSE_STYLE_RULES_CONTENT, 'utf-8');
258
264
  return true;
259
265
  }
@@ -2,6 +2,7 @@ import { generateRules, detectAgentPlatformFromFiles } from '../../engine/skill-
2
2
  import { knowledgeStore } from '../../storage/index.js';
3
3
  import { writeFile, access, mkdir } from 'node:fs/promises';
4
4
  import { join, dirname } from 'node:path';
5
+ import { assertEnglishOnlyArtifactText } from '../../engine/spec-language/english-only.js';
5
6
  /**
6
7
  * Detect agent platform, generate rules, and write them to disk.
7
8
  * Only writes files that don't exist yet — never overwrites user custom rules.
@@ -22,6 +23,7 @@ export async function runRulesWriter(projectPath, projectId, knowledge, recommen
22
23
  }
23
24
  catch {
24
25
  await mkdir(dirname(rulesFilePath), { recursive: true });
26
+ assertEnglishOnlyArtifactText(generatedRules.rulesFile.content, 'rule');
25
27
  await writeFile(rulesFilePath, generatedRules.rulesFile.content, 'utf-8');
26
28
  rulesWritten = true;
27
29
  }
@@ -34,6 +36,7 @@ export async function runRulesWriter(projectPath, projectId, knowledge, recommen
34
36
  }
35
37
  catch {
36
38
  await mkdir(dirname(filePath), { recursive: true });
39
+ assertEnglishOnlyArtifactText(file.content, file.path.includes('skill') ? 'skill' : 'rule');
37
40
  await writeFile(filePath, file.content, 'utf-8');
38
41
  additionalFilesWritten.push(file.path);
39
42
  }
@@ -3,6 +3,7 @@
3
3
  import { writeFile, mkdir, access, readFile } from 'node:fs/promises';
4
4
  import { join, dirname } from 'node:path';
5
5
  import { fileURLToPath } from 'node:url';
6
+ import { assertEnglishOnlyArtifactText } from '../../engine/spec-language/english-only.js';
6
7
  const SKILL_TARGET_PATH = '.claude/skills/planu-multi-teammate-review.md';
7
8
  // Resolve the template path relative to this file (works after tsc compilation too)
8
9
  function resolveTemplatePath() {
@@ -48,6 +49,7 @@ export async function generateMultiTeammateReviewSkillIfMissing(projectPath) {
48
49
  const skillsDir = join(projectPath, '.claude/skills');
49
50
  await mkdir(skillsDir, { recursive: true });
50
51
  const content = await readTemplate();
52
+ assertEnglishOnlyArtifactText(content, 'skill');
51
53
  await writeFile(skillPath, content, 'utf-8');
52
54
  return true;
53
55
  }
@@ -2,6 +2,7 @@
2
2
  // SPEC-519: Generate compact skill file for dense SDD mode
3
3
  import { writeFile, mkdir, access } from 'node:fs/promises';
4
4
  import { join } from 'node:path';
5
+ import { assertEnglishOnlyArtifactText } from '../../engine/spec-language/english-only.js';
5
6
  const COMPACT_SKILL_PATH = '.claude/skills/compact.md';
6
7
  const COMPACT_SKILL_CONTENT = `# /compact — Dense SDD Mode
7
8
 
@@ -43,6 +44,7 @@ export async function generateCompactSkillIfMissing(projectPath) {
43
44
  catch {
44
45
  const skillsDir = join(projectPath, '.claude/skills');
45
46
  await mkdir(skillsDir, { recursive: true });
47
+ assertEnglishOnlyArtifactText(COMPACT_SKILL_CONTENT, 'skill');
46
48
  await writeFile(skillPath, COMPACT_SKILL_CONTENT, 'utf-8');
47
49
  return true;
48
50
  }
@@ -0,0 +1,8 @@
1
+ export type EnglishOnlyArtifactKind = 'spec' | 'skill' | 'agent' | 'rule';
2
+ export interface EnglishOnlyValidationResult {
3
+ ok: boolean;
4
+ detectedLanguage: 'en' | 'es' | 'pt' | 'unknown';
5
+ reason?: string;
6
+ signals: string[];
7
+ }
8
+ //# sourceMappingURL=spec-language.d.ts.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=spec-language.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@planu/cli",
3
- "version": "4.0.0",
3
+ "version": "4.1.1",
4
4
  "description": "Planu — MCP Server for Spec Driven Development with native Rust acceleration for hot paths. Cross-platform (Linux/macOS/Windows, x64/arm64, glibc/musl).",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -32,12 +32,12 @@
32
32
  "packageName": "@planu/core"
33
33
  },
34
34
  "optionalDependencies": {
35
- "@planu/core-darwin-arm64": "3.9.14",
36
- "@planu/core-darwin-x64": "3.9.14",
37
- "@planu/core-linux-arm64-gnu": "3.9.14",
38
- "@planu/core-linux-arm64-musl": "3.9.14",
39
- "@planu/core-linux-x64-gnu": "3.9.14",
40
- "@planu/core-linux-x64-musl": "3.9.14"
35
+ "@planu/core-darwin-arm64": "4.1.1",
36
+ "@planu/core-darwin-x64": "4.1.1",
37
+ "@planu/core-linux-arm64-gnu": "4.1.1",
38
+ "@planu/core-linux-arm64-musl": "4.1.1",
39
+ "@planu/core-linux-x64-gnu": "4.1.1",
40
+ "@planu/core-linux-x64-musl": "4.1.1"
41
41
  },
42
42
  "engines": {
43
43
  "node": ">=24.0.0"
@@ -68,6 +68,7 @@
68
68
  "test:integration": "vitest run tests/integration",
69
69
  "check": "pnpm typecheck && pnpm lint && pnpm format:check",
70
70
  "check:strict": "pnpm typecheck && pnpm lint && pnpm format:check && pnpm audit:deadcode && pnpm audit:circular && pnpm audit:types && pnpm audit:security && pnpm audit:licenses && pnpm audit:i18n",
71
+ "check:deps:fresh": "bash scripts/check-dependency-freshness.sh",
71
72
  "audit:deadcode": "knip",
72
73
  "audit:circular": "madge --circular --extensions ts src/",
73
74
  "audit:types": "type-coverage --at-least 98 --ignore-catch --strict --ignore-files 'tests/**'",
@@ -149,8 +150,8 @@
149
150
  "@commitlint/config-conventional": "^21.0.1",
150
151
  "@eslint/js": "^10.0.1",
151
152
  "@napi-rs/cli": "^3.6.2",
152
- "@secretlint/secretlint-rule-no-homedir": "^13.0.0",
153
- "@secretlint/secretlint-rule-preset-recommend": "^13.0.0",
153
+ "@secretlint/secretlint-rule-no-homedir": "^13.0.2",
154
+ "@secretlint/secretlint-rule-preset-recommend": "^13.0.2",
154
155
  "@semantic-release/changelog": "^6.0.3",
155
156
  "@semantic-release/commit-analyzer": "^13.0.1",
156
157
  "@semantic-release/git": "^10.0.1",
@@ -159,30 +160,30 @@
159
160
  "@semantic-release/release-notes-generator": "^14.1.1",
160
161
  "@stryker-mutator/core": "^9.6.1",
161
162
  "@stryker-mutator/vitest-runner": "^9.6.1",
162
- "@supabase/supabase-js": "^2.105.4",
163
- "@types/node": "^25.7.0",
164
- "@vitejs/plugin-vue": "^6.0.6",
165
- "@vitest/coverage-v8": "^4.1.6",
163
+ "@supabase/supabase-js": "^2.106.1",
164
+ "@types/node": "^25.9.1",
165
+ "@vitejs/plugin-vue": "^6.0.7",
166
+ "@vitest/coverage-v8": "^4.1.7",
166
167
  "@vue/test-utils": "^2.4.10",
167
- "eslint": "^10.3.0",
168
+ "eslint": "^10.4.0",
168
169
  "eslint-config-prettier": "^10.1.8",
169
170
  "eslint-import-resolver-typescript": "^4.4.4",
170
171
  "eslint-plugin-import": "^2.32.0",
171
172
  "happy-dom": "^20.9.0",
172
173
  "husky": "^9.1.7",
173
174
  "javascript-obfuscator": "^5.4.2",
174
- "knip": "^6.13.1",
175
- "lint-staged": "^17.0.4",
175
+ "knip": "^6.14.1",
176
+ "lint-staged": "^17.0.5",
176
177
  "madge": "^8.0.0",
177
178
  "prettier": "^3.8.3",
178
- "secretlint": "^13.0.0",
179
+ "secretlint": "^13.0.2",
179
180
  "semantic-release": "^25.0.3",
180
181
  "tsc-alias": "^1.8.17",
181
182
  "type-coverage": "^2.29.7",
182
183
  "typescript": "^6.0.3",
183
- "typescript-eslint": "^8.59.3",
184
- "vite": "^8.0.12",
185
- "vitest": "^4.1.6",
184
+ "typescript-eslint": "^8.59.4",
185
+ "vite": "^8.0.14",
186
+ "vitest": "^4.1.7",
186
187
  "vue": "^3.5.34"
187
188
  }
188
189
  }