@planu/cli 3.9.6 → 3.9.8

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 (37) hide show
  1. package/dist/config/skill-templates/planu-context-assets.md +94 -0
  2. package/dist/engine/handoff-packager.js +151 -4
  3. package/dist/engine/sdd-model-routing.d.ts +16 -0
  4. package/dist/engine/sdd-model-routing.js +195 -0
  5. package/dist/engine/universal-rules/catalog.js +10 -0
  6. package/dist/engine/universal-rules/installer.js +9 -3
  7. package/dist/engine/universal-rules/rules/planu-approval-gates.d.ts +3 -0
  8. package/dist/engine/universal-rules/rules/planu-approval-gates.js +41 -0
  9. package/dist/engine/universal-rules/rules/planu-bdd-criteria.d.ts +3 -0
  10. package/dist/engine/universal-rules/rules/planu-bdd-criteria.js +45 -0
  11. package/dist/engine/universal-rules/rules/planu-english-specs.d.ts +3 -0
  12. package/dist/engine/universal-rules/rules/planu-english-specs.js +36 -0
  13. package/dist/engine/universal-rules/rules/planu-release-policy.d.ts +3 -0
  14. package/dist/engine/universal-rules/rules/planu-release-policy.js +38 -0
  15. package/dist/engine/universal-rules/rules/planu-sdd-model-routing.d.ts +3 -0
  16. package/dist/engine/universal-rules/rules/planu-sdd-model-routing.js +51 -0
  17. package/dist/tools/init-project/host-assets-writer.d.ts +21 -0
  18. package/dist/tools/init-project/host-assets-writer.js +171 -0
  19. package/dist/tools/init-project/scaffold-writer.d.ts +8 -0
  20. package/dist/tools/init-project/scaffold-writer.js +122 -74
  21. package/dist/tools/package-handoff.js +76 -0
  22. package/dist/tools/update-status/file-sync.d.ts +1 -0
  23. package/dist/tools/update-status/file-sync.js +1 -0
  24. package/dist/tools/update-status/index.js +88 -0
  25. package/dist/types/index.d.ts +1 -0
  26. package/dist/types/index.js +1 -0
  27. package/dist/types/readiness.d.ts +10 -1
  28. package/dist/types/sdd-model-routing.d.ts +22 -0
  29. package/dist/types/sdd-model-routing.js +2 -0
  30. package/dist/types/spec/inputs.d.ts +23 -0
  31. package/package.json +7 -7
  32. package/dist/types/data/estimation.d.ts +0 -147
  33. package/dist/types/data/estimation.js +0 -2
  34. package/dist/types/data/index.d.ts +0 -5
  35. package/dist/types/data/index.js +0 -6
  36. package/dist/types/data/velocity.d.ts +0 -168
  37. package/dist/types/data/velocity.js +0 -4
@@ -0,0 +1,94 @@
1
+ ---
2
+ name: planu-context-assets
3
+ description: Create project-specific Planu rules or skills from the current app context so recurring standards and workflows become reusable agent guidance instead of MCP tool bloat.
4
+ triggers:
5
+ - create project rules
6
+ - create project skills
7
+ - capture this workflow
8
+ - make this a rule
9
+ - make this a skill
10
+ - codify this pattern
11
+ - project context changed
12
+ - app conventions
13
+ version: 1.0.0
14
+ ---
15
+
16
+ # /planu-context-assets — Create Rules or Skills from App Context
17
+
18
+ ## When to invoke
19
+
20
+ Use this skill when project context reveals a recurring convention, workflow, integration, domain rule, or implementation pattern that future agents should reuse.
21
+
22
+ Examples:
23
+
24
+ - A design system rule is discovered while implementing UI.
25
+ - A release or deployment workflow becomes stable.
26
+ - A legacy cleanup pattern repeats across modules.
27
+ - A domain invariant must never be violated.
28
+ - A framework-specific project convention is discovered during analysis.
29
+
30
+ ## Decision rule
31
+
32
+ Create a **rule** when the guidance is mandatory and should constrain every future agent:
33
+
34
+ - architecture boundaries
35
+ - spec quality gates
36
+ - release policy
37
+ - BDD/SDD invariants
38
+ - security or data handling rules
39
+ - "never do X" / "always do Y" project standards
40
+
41
+ Create a **skill** when the guidance is an optional workflow invoked for a specific task:
42
+
43
+ - release workflow
44
+ - Figma implementation workflow
45
+ - legacy characterization workflow
46
+ - domain bundle generation
47
+ - integration setup
48
+ - migration playbook
49
+ - debugging or audit procedure
50
+
51
+ Do not create either when the information is one-off, speculative, or already covered by an existing rule/skill.
52
+
53
+ ## Workflow
54
+
55
+ 1. Inspect the current context and existing project assets:
56
+ - `AGENTS.md`
57
+ - `CLAUDE.md`
58
+ - `.claude/rules/`
59
+ - `.claude/skills/`
60
+ - `.gemini/conventions.md`
61
+ - `.gemini/skills/`
62
+ - `.opencode/rules/`
63
+ - `.opencode/skills/`
64
+ 2. Decide whether the new asset is a rule, a skill, or nothing.
65
+ 3. Keep the asset small and operational:
66
+ - one purpose
67
+ - explicit trigger
68
+ - concrete steps or constraints
69
+ - verification command when applicable
70
+ 4. Use Planu host-aware tools:
71
+ - `create_rule` for mandatory constraints
72
+ - `create_skill` for reusable workflows
73
+ 5. Use `host: "auto"` unless the user explicitly targets a host.
74
+ 6. Prefer updating an existing Planu-owned asset over creating a duplicate.
75
+
76
+ ## Quality bar
77
+
78
+ - Rules must be enforceable and written as constraints.
79
+ - Skills must describe when to invoke, what to inspect, what to do, and what output to produce.
80
+ - Do not reference legacy MCP tools that are not part of the 14-tool public core.
81
+ - Do not create broad "misc" skills.
82
+ - Do not preserve dead workflows "just in case".
83
+
84
+ ## Output
85
+
86
+ Report:
87
+
88
+ - asset type: rule or skill
89
+ - asset name
90
+ - host target
91
+ - path or file updated
92
+ - why it should exist
93
+ - when it should trigger
94
+
@@ -1,5 +1,7 @@
1
1
  // engine/handoff-packager.ts — Assembles handoff context package for AI agents (SPEC-039)
2
- import { readFile } from 'node:fs/promises';
2
+ import { createHash } from 'node:crypto';
3
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
4
+ import { join } from 'node:path';
3
5
  import { stripFrontmatter } from './frontmatter-parser.js';
4
6
  // ── Parsing helpers ──────────────────────────────────────────────────────────
5
7
  async function safeReadFile(path) {
@@ -41,11 +43,37 @@ function extractCriteria(huContent) {
41
43
  if (!huContent) {
42
44
  return [];
43
45
  }
46
+ const bddBlocks = extractBddCriteria(huContent);
47
+ if (bddBlocks.length > 0) {
48
+ return bddBlocks;
49
+ }
44
50
  return huContent
45
51
  .split('\n')
46
52
  .filter((line) => /^- \[[ x]\]/.test(line.trim()))
47
53
  .map((line) => line.trim().replace(/^- \[[ x]\]\s*/, ''));
48
54
  }
55
+ function extractBddCriteria(content) {
56
+ const criteria = [];
57
+ let current = [];
58
+ for (const rawLine of content.split('\n')) {
59
+ const trimmed = rawLine.trim().replace(/^-+\s*/, '');
60
+ if (/^(GIVEN|WHEN|THEN|AND)\b/i.test(trimmed)) {
61
+ if (/^GIVEN\b/i.test(trimmed) && current.length > 0) {
62
+ criteria.push(current.join(' / '));
63
+ current = [];
64
+ }
65
+ current.push(trimmed);
66
+ }
67
+ else if (current.length > 0 && trimmed === '') {
68
+ criteria.push(current.join(' / '));
69
+ current = [];
70
+ }
71
+ }
72
+ if (current.length > 0) {
73
+ criteria.push(current.join(' / '));
74
+ }
75
+ return criteria.filter((criterion) => /GIVEN\b/i.test(criterion) && /WHEN\b/i.test(criterion) && /THEN\b/i.test(criterion));
76
+ }
49
77
  function extractFilesFromFicha(fichaContent) {
50
78
  if (!fichaContent) {
51
79
  return { toModify: [], toCreate: [] };
@@ -88,6 +116,35 @@ function extractFilesFromFicha(fichaContent) {
88
116
  }
89
117
  return { toModify, toCreate };
90
118
  }
119
+ function extractBacktickedFiles(content) {
120
+ const files = new Set();
121
+ const matches = content.matchAll(/`([^`\n]+\.(?:ts|tsx|js|jsx|mjs|cjs|json|md|yml|yaml|sh|css|scss|html))`/g);
122
+ for (const match of matches) {
123
+ const filePath = match[1]?.trim();
124
+ if (filePath && !filePath.includes(' ')) {
125
+ files.add(filePath);
126
+ }
127
+ }
128
+ return [...files];
129
+ }
130
+ function extractLinesByKeywords(content, keywords) {
131
+ return content
132
+ .split('\n')
133
+ .map((line) => line.trim().replace(/^[-*]\s*/, ''))
134
+ .filter((line) => line.length > 0 && keywords.test(line))
135
+ .slice(0, 12);
136
+ }
137
+ function extractOperationalSections(content, spec) {
138
+ return {
139
+ testPlan: extractLinesByKeywords(content, /\b(test|typecheck|lint|vitest|playwright|verification|validate|pnpm|npm)\b/i),
140
+ risks: extractLinesByKeywords(content, /\b(risk|edge|failure|security|drift|migration|breaking|compatibility|rollback)\b/i),
141
+ ownership: extractLinesByKeywords(content, /\b(owner|ownership|wave|agent|reviewer|arbiter|files?|responsible)\b/i),
142
+ currentState: `Spec ${spec.id} is ${spec.status}; implementation must follow the approved spec artifact, not chat history.`,
143
+ nextAction: spec.status === 'approved'
144
+ ? 'Move to implementing with this handoff, then implement only the listed scope.'
145
+ : 'Review the current spec status and move through the required SDD gates before implementation.',
146
+ };
147
+ }
91
148
  function extractOutOfScope(huContent, spec) {
92
149
  if (!huContent) {
93
150
  return [
@@ -155,13 +212,24 @@ export async function packageHandoff(spec, knowledge) {
155
212
  }
156
213
  const objective = extractObjective(huContent, spec);
157
214
  const criteria = extractCriteria(huContent);
158
- const { toModify, toCreate } = extractFilesFromFicha(fichaContent);
215
+ const parsedFiles = extractFilesFromFicha(fichaContent);
216
+ const discoveredFiles = extractBacktickedFiles(`${huContent}\n${fichaContent}`);
217
+ const toModify = parsedFiles.toModify.length > 0 ? parsedFiles.toModify : discoveredFiles;
218
+ const toCreate = parsedFiles.toCreate;
159
219
  const outOfScope = extractOutOfScope(huContent, spec);
160
220
  const constraints = buildConstraints(knowledge);
221
+ const operational = extractOperationalSections(`${huContent}\n${fichaContent}`, spec);
161
222
  if (criteria.length === 0) {
162
- warnings.push('No acceptance criteria found in HU.mdcriteria list will be empty');
223
+ warnings.push('No acceptance criteria found with complete BDD structure implementation handoff is blocked');
163
224
  }
164
- return {
225
+ const blockers = [
226
+ ...(criteria.length === 0 ? ['Missing BDD acceptance criteria'] : []),
227
+ ...(toModify.length + toCreate.length === 0 ? ['Missing file ownership / target files'] : []),
228
+ ...(operational.testPlan.length === 0 ? ['Missing test plan'] : []),
229
+ ...(operational.risks.length === 0 ? ['Missing risk analysis'] : []),
230
+ ...(operational.ownership.length === 0 ? ['Missing ownership'] : []),
231
+ ];
232
+ const pkg = {
165
233
  specId: spec.id,
166
234
  generatedAt: new Date().toISOString(),
167
235
  readinessScore: 0, // caller can populate with checkSpecReadiness result
@@ -169,9 +237,88 @@ export async function packageHandoff(spec, knowledge) {
169
237
  criteria,
170
238
  filesToModify: toModify,
171
239
  filesToCreate: toCreate,
240
+ ownership: operational.ownership,
241
+ testPlan: operational.testPlan,
242
+ risks: operational.risks,
172
243
  outOfScope,
244
+ currentState: operational.currentState,
245
+ nextAction: operational.nextAction,
173
246
  constraints,
174
247
  warnings,
248
+ blocked: blockers.length > 0,
249
+ blockers,
175
250
  };
251
+ return persistHandoffIfPossible(pkg, knowledge);
252
+ }
253
+ function renderPersistedHandoff(pkg) {
254
+ return [
255
+ `# Implementation Handoff Package — ${pkg.specId}`,
256
+ '',
257
+ `Generated: ${pkg.generatedAt}`,
258
+ `Blocked: ${pkg.blocked ? 'yes' : 'no'}`,
259
+ '',
260
+ '## Objective',
261
+ pkg.objective,
262
+ '',
263
+ '## Acceptance Criteria (BDD)',
264
+ ...pkg.criteria.map((criterion) => `- ${criterion}`),
265
+ '',
266
+ '## Files To Modify',
267
+ ...pkg.filesToModify.map((filePath) => `- ${filePath}`),
268
+ '',
269
+ '## Files To Create',
270
+ ...pkg.filesToCreate.map((filePath) => `- ${filePath}`),
271
+ '',
272
+ '## Ownership',
273
+ ...pkg.ownership.map((item) => `- ${item}`),
274
+ '',
275
+ '## Test Plan',
276
+ ...pkg.testPlan.map((item) => `- ${item}`),
277
+ '',
278
+ '## Risks',
279
+ ...pkg.risks.map((item) => `- ${item}`),
280
+ '',
281
+ '## Out Of Scope',
282
+ ...pkg.outOfScope.map((item) => `- ${item}`),
283
+ '',
284
+ '## Current State',
285
+ pkg.currentState,
286
+ '',
287
+ '## Next Action',
288
+ pkg.nextAction,
289
+ '',
290
+ '## Blockers',
291
+ ...(pkg.blockers.length > 0 ? pkg.blockers.map((item) => `- ${item}`) : ['- None']),
292
+ '',
293
+ ].join('\n');
294
+ }
295
+ async function persistHandoffIfPossible(pkg, knowledge) {
296
+ if (!knowledge.projectPath) {
297
+ return pkg;
298
+ }
299
+ const handoffDir = join(knowledge.projectPath, 'planu', 'handoffs');
300
+ const handoffPath = join(handoffDir, `${pkg.specId}.md`);
301
+ const sessionContextPath = join(knowledge.projectPath, 'planu', 'session-context.md');
302
+ const markdown = renderPersistedHandoff(pkg);
303
+ try {
304
+ await mkdir(handoffDir, { recursive: true });
305
+ await writeFile(handoffPath, markdown, 'utf-8');
306
+ }
307
+ catch {
308
+ return {
309
+ ...pkg,
310
+ warnings: [
311
+ ...pkg.warnings,
312
+ 'Could not persist handoff artifact — context gate will block implementing.',
313
+ ],
314
+ blocked: true,
315
+ blockers: [...pkg.blockers, 'Handoff artifact could not be persisted'],
316
+ };
317
+ }
318
+ const contextContent = await readFile(sessionContextPath, 'utf-8').catch(() => '');
319
+ const contextHash = createHash('sha256')
320
+ .update(`${contextContent}\n---HANDOFF---\n${markdown}`, 'utf8')
321
+ .digest('hex');
322
+ return { ...pkg, handoffPath, contextHash };
176
323
  }
177
324
  //# sourceMappingURL=handoff-packager.js.map
@@ -0,0 +1,16 @@
1
+ import type { SpecStatus, UpdateStatusInput } from '../types/index.js';
2
+ import type { SddModelTier, SddLifecyclePhase, SddRoutingGateResult, TransitionEvidenceMeta } from '../types/sdd-model-routing.js';
3
+ export declare function requiredTierForPhase(phase: SddLifecyclePhase): SddModelTier;
4
+ export declare function phaseForTransition(status: SpecStatus): SddLifecyclePhase | null;
5
+ export declare function classifySddModelTier(modelId: string | undefined): SddModelTier | 'unknown';
6
+ export declare function hashContextPackage(input: {
7
+ projectPath: string;
8
+ handoffPath?: string;
9
+ }): Promise<string>;
10
+ export declare function checkSddModelRoutingGate(input: {
11
+ params: UpdateStatusInput;
12
+ status: SpecStatus;
13
+ projectPath?: string;
14
+ }): Promise<SddRoutingGateResult>;
15
+ export declare function buildTransitionEvidenceMeta(params: UpdateStatusInput): TransitionEvidenceMeta;
16
+ //# sourceMappingURL=sdd-model-routing.d.ts.map
@@ -0,0 +1,195 @@
1
+ // engine/sdd-model-routing.ts — Hard gates for SDD model tiers and context continuity.
2
+ import { createHash } from 'node:crypto';
3
+ import { readFile } from 'node:fs/promises';
4
+ import { join } from 'node:path';
5
+ import { classifyModelTier } from './model-tier-resolver.js';
6
+ const FORCE_REASON_MIN_LENGTH = 100;
7
+ export function requiredTierForPhase(phase) {
8
+ if (phase === 'analysis' || phase === 'arbitration') {
9
+ return 'max';
10
+ }
11
+ if (phase === 'review') {
12
+ return 'review';
13
+ }
14
+ return 'implementation';
15
+ }
16
+ export function phaseForTransition(status) {
17
+ if (status === 'approved') {
18
+ return 'analysis';
19
+ }
20
+ if (status === 'implementing') {
21
+ return 'implementation';
22
+ }
23
+ if (status === 'done') {
24
+ return 'arbitration';
25
+ }
26
+ return null;
27
+ }
28
+ export function classifySddModelTier(modelId) {
29
+ if (!modelId) {
30
+ return 'unknown';
31
+ }
32
+ const id = modelId.toLowerCase();
33
+ if (/opus|ultra|o1|o3|gemini.*(?:ultra|pro)|gpt-5(?:[._-]?5)?(?!.*(?:mini|nano|small|flash))/.test(id)) {
34
+ return 'max';
35
+ }
36
+ const modelTier = classifyModelTier(modelId);
37
+ if (modelTier === 'haiku') {
38
+ return 'implementation';
39
+ }
40
+ if (modelTier === 'opus') {
41
+ return 'max';
42
+ }
43
+ return 'review';
44
+ }
45
+ function hasTierEvidence(params, required) {
46
+ if (params.modelTierUsed === required) {
47
+ return true;
48
+ }
49
+ const classified = classifySddModelTier(params.modelId);
50
+ if (required === 'max') {
51
+ return classified === 'max';
52
+ }
53
+ if (required === 'implementation') {
54
+ return classified === 'implementation';
55
+ }
56
+ return classified === 'review' || classified === 'max';
57
+ }
58
+ function forceReason(params) {
59
+ const reason = params.forceStatusReason ?? params.reason ?? '';
60
+ return reason.trim().length >= FORCE_REASON_MIN_LENGTH ? reason.trim() : null;
61
+ }
62
+ function canForce(params, status) {
63
+ const forceFlag = status === 'approved'
64
+ ? params.forceApprove === true
65
+ : status === 'done'
66
+ ? params.forceStatus === true || params.force === true
67
+ : params.force === true;
68
+ return forceFlag && forceReason(params) !== null;
69
+ }
70
+ function buildBlock(status, blockers, fixHint) {
71
+ return {
72
+ content: [
73
+ {
74
+ type: 'text',
75
+ text: JSON.stringify({
76
+ error: 'SDD_MODEL_ROUTING_GATE_FAILED',
77
+ status,
78
+ blockers,
79
+ fixHint,
80
+ }, null, 2),
81
+ },
82
+ ],
83
+ isError: true,
84
+ structuredContent: {
85
+ error: 'SDD_MODEL_ROUTING_GATE_FAILED',
86
+ status,
87
+ blockers,
88
+ fixHint,
89
+ },
90
+ };
91
+ }
92
+ export async function hashContextPackage(input) {
93
+ const parts = [];
94
+ const sessionContextPath = join(input.projectPath, 'planu', 'session-context.md');
95
+ parts.push(await readFile(sessionContextPath, 'utf-8'));
96
+ if (input.handoffPath) {
97
+ parts.push(await readFile(input.handoffPath, 'utf-8'));
98
+ }
99
+ return createHash('sha256').update(parts.join('\n---HANDOFF---\n'), 'utf8').digest('hex');
100
+ }
101
+ async function contextCanBeReconstructed(params) {
102
+ const blockers = [];
103
+ if (!params.projectPath) {
104
+ blockers.push('projectPath is required to reconstruct planu/session-context.md.');
105
+ return blockers;
106
+ }
107
+ const sessionContextPath = join(params.projectPath, 'planu', 'session-context.md');
108
+ try {
109
+ const raw = await readFile(sessionContextPath, 'utf-8');
110
+ if (raw.trim().length < 80) {
111
+ blockers.push('planu/session-context.md exists but is too small to reconstruct context.');
112
+ }
113
+ }
114
+ catch {
115
+ blockers.push('planu/session-context.md is missing or unreadable.');
116
+ }
117
+ if (params.handoffPath) {
118
+ try {
119
+ const raw = await readFile(params.handoffPath, 'utf-8');
120
+ if (raw.trim().length < 200) {
121
+ blockers.push('handoffPath exists but is too small to be an operational package.');
122
+ }
123
+ }
124
+ catch {
125
+ blockers.push('handoffPath is missing or unreadable.');
126
+ }
127
+ }
128
+ else if (!params.handoffArtifactId) {
129
+ blockers.push('handoffPath or handoffArtifactId is required.');
130
+ }
131
+ return blockers;
132
+ }
133
+ export async function checkSddModelRoutingGate(input) {
134
+ const { params, status, projectPath } = input;
135
+ const phase = phaseForTransition(status);
136
+ const gateResults = {};
137
+ const forcedReasons = [];
138
+ if (phase === null) {
139
+ return { blockResult: null, gateResults: { sddModelRouting: 'skip' }, forcedReasons };
140
+ }
141
+ const requiredTier = requiredTierForPhase(phase);
142
+ const blockers = [];
143
+ if (!hasTierEvidence(params, requiredTier)) {
144
+ blockers.push(`Transition to ${status} requires model tier "${requiredTier}" evidence. Received modelTierUsed="${params.modelTierUsed ?? 'missing'}", modelId="${params.modelId ?? 'missing'}".`);
145
+ }
146
+ if (status === 'implementing' || status === 'done') {
147
+ if (!params.contextHash) {
148
+ blockers.push('contextHash is required so the next agent can verify context continuity.');
149
+ }
150
+ blockers.push(...(await contextCanBeReconstructed({
151
+ projectPath,
152
+ handoffPath: params.handoffPath,
153
+ handoffArtifactId: params.handoffArtifactId,
154
+ })));
155
+ }
156
+ if (status === 'done') {
157
+ if (!params.reviewedBy || params.reviewedBy.trim().length === 0) {
158
+ blockers.push('reviewedBy is required before done.');
159
+ }
160
+ if (!params.arbitratedBy || params.arbitratedBy.trim().length === 0) {
161
+ blockers.push('arbitratedBy is required before done.');
162
+ }
163
+ if (params.reconcileRequired === true) {
164
+ blockers.push('reconcileRequired=true blocks done until reconcile_spec records the intentional drift.');
165
+ }
166
+ }
167
+ if (blockers.length === 0) {
168
+ gateResults.sddModelRouting = 'pass';
169
+ return { blockResult: null, gateResults, forcedReasons };
170
+ }
171
+ if (canForce(params, status)) {
172
+ gateResults.sddModelRouting = 'forced';
173
+ forcedReasons.push(forceReason(params) ?? 'Forced SDD model/context gate');
174
+ return { blockResult: null, gateResults, forcedReasons };
175
+ }
176
+ gateResults.sddModelRouting = 'fail';
177
+ return {
178
+ blockResult: buildBlock(status, blockers, 'Provide modelTierUsed/modelId evidence plus package_handoff context evidence, or force with a >=100 character audited reason.'),
179
+ gateResults,
180
+ forcedReasons,
181
+ };
182
+ }
183
+ export function buildTransitionEvidenceMeta(params) {
184
+ return {
185
+ ...(params.modelTierUsed !== undefined && { modelTierUsed: params.modelTierUsed }),
186
+ ...(params.modelId !== undefined && { modelId: params.modelId }),
187
+ ...(params.contextHash !== undefined && { contextHash: params.contextHash }),
188
+ ...(params.handoffPath !== undefined && { handoffPath: params.handoffPath }),
189
+ ...(params.handoffArtifactId !== undefined && { handoffArtifactId: params.handoffArtifactId }),
190
+ ...(params.reviewedBy !== undefined && { reviewedBy: params.reviewedBy }),
191
+ ...(params.arbitratedBy !== undefined && { arbitratedBy: params.arbitratedBy }),
192
+ ...(params.reconcileRequired !== undefined && { reconcileRequired: params.reconcileRequired }),
193
+ };
194
+ }
195
+ //# sourceMappingURL=sdd-model-routing.js.map
@@ -4,12 +4,22 @@ import { planuDogfoodBugsRule } from './rules/planu-dogfood-bugs.js';
4
4
  import { planuWorkflowRule } from './rules/planu-workflow.js';
5
5
  import { planuModesRule } from './rules/planu-modes.js';
6
6
  import { agentTeamsRule } from './rules/agent-teams.js';
7
+ import { planuEnglishSpecsRule } from './rules/planu-english-specs.js';
8
+ import { planuBddCriteriaRule } from './rules/planu-bdd-criteria.js';
9
+ import { planuApprovalGatesRule } from './rules/planu-approval-gates.js';
10
+ import { planuReleasePolicyRule } from './rules/planu-release-policy.js';
11
+ import { planuSddModelRoutingRule } from './rules/planu-sdd-model-routing.js';
7
12
  /**
8
13
  * The full catalog of universal Planu rules.
9
14
  * Order matters: rules are installed in catalog order.
10
15
  */
11
16
  export const UNIVERSAL_RULES = [
12
17
  planuWorkflowRule,
18
+ planuEnglishSpecsRule,
19
+ planuBddCriteriaRule,
20
+ planuApprovalGatesRule,
21
+ planuSddModelRoutingRule,
22
+ planuReleasePolicyRule,
13
23
  planuModesRule,
14
24
  agentTeamsRule,
15
25
  planuDogfoodBugsRule,
@@ -6,8 +6,14 @@ import { UNIVERSAL_RULES } from './catalog.js';
6
6
  import { writeRuleForHost } from './host-writer.js';
7
7
  import { hashContent, readManifest, upsertManifestEntry } from './user-edit-detector.js';
8
8
  /** Path of the manifest relative to projectPath. */
9
- function manifestPath(projectPath) {
10
- return join(projectPath, '.claude', 'rules', '.planu-rules-manifest.json');
9
+ function manifestPath(projectPath, host) {
10
+ if (host === 'claude-code') {
11
+ return join(projectPath, '.claude', 'rules', '.planu-rules-manifest.json');
12
+ }
13
+ if (host === 'gemini') {
14
+ return join(projectPath, '.gemini', '.planu-rules-manifest.json');
15
+ }
16
+ return join(projectPath, '.planu', 'rules-manifest.codex.json');
11
17
  }
12
18
  /**
13
19
  * Install all default-enabled universal rules applicable to `host`.
@@ -15,7 +21,7 @@ function manifestPath(projectPath) {
15
21
  * Returns the list of rules that were written.
16
22
  */
17
23
  export async function installUniversalRules(projectPath, host) {
18
- const mPath = manifestPath(projectPath);
24
+ const mPath = manifestPath(projectPath, host);
19
25
  const manifest = (await readManifest(mPath)) ?? { rules: [] };
20
26
  const installed = [];
21
27
  for (const rule of UNIVERSAL_RULES) {
@@ -0,0 +1,3 @@
1
+ import type { UniversalRule } from '../../../types/universal-rules/index.js';
2
+ export declare const planuApprovalGatesRule: UniversalRule;
3
+ //# sourceMappingURL=planu-approval-gates.d.ts.map
@@ -0,0 +1,41 @@
1
+ // engine/universal-rules/rules/planu-approval-gates.ts — Universal rule: approval and done gates
2
+ function buildBody() {
3
+ return `# Planu Approval and Done Gates
4
+
5
+ Auto-generated by \`init_project\`. Do not edit manually.
6
+
7
+ ## Approval Gate
8
+
9
+ Before moving a spec to \`approved\`, run and pass:
10
+
11
+ 1. \`challenge_spec\`
12
+ 2. \`check_readiness\`
13
+ 3. BDD criteria completeness
14
+ 4. files-to-create / files-to-modify ownership
15
+ 5. test plan and verification commands
16
+
17
+ If the user explicitly forces approval, record the reason in the audit trail and make the missing risk visible.
18
+
19
+ ## Done Gate
20
+
21
+ Before moving a spec to \`done\`, run \`validate\`.
22
+
23
+ If implementation intentionally diverged from the approved spec, run \`reconcile_spec\` first and make the divergence explicit before marking done.
24
+
25
+ ## Hard Blocks
26
+
27
+ - Do not approve specs with placeholders.
28
+ - Do not mark done without validation evidence.
29
+ - Do not hide intentional drift; reconcile it.
30
+ `;
31
+ }
32
+ export const planuApprovalGatesRule = {
33
+ id: 'planu-approval-gates',
34
+ name: 'Planu Approval Gates',
35
+ description: 'Requires challenge/readiness before approval and validate/reconcile before done.',
36
+ category: 'safety',
37
+ applicableHosts: ['all'],
38
+ defaultEnabled: true,
39
+ buildContent: (_host) => buildBody(),
40
+ };
41
+ //# sourceMappingURL=planu-approval-gates.js.map
@@ -0,0 +1,3 @@
1
+ import type { UniversalRule } from '../../../types/universal-rules/index.js';
2
+ export declare const planuBddCriteriaRule: UniversalRule;
3
+ //# sourceMappingURL=planu-bdd-criteria.d.ts.map
@@ -0,0 +1,45 @@
1
+ // engine/universal-rules/rules/planu-bdd-criteria.ts — Universal rule: BDD acceptance criteria
2
+ function buildBody() {
3
+ return `# Planu BDD Acceptance Criteria
4
+
5
+ Auto-generated by \`init_project\`. Do not edit manually.
6
+
7
+ ## Rule
8
+
9
+ Every acceptance criterion must be executable enough for an implementation agent and a validation gate.
10
+
11
+ Required format:
12
+
13
+ \`\`\`gherkin
14
+ GIVEN <initial state or context>
15
+ WHEN <action or event>
16
+ THEN <observable result>
17
+ AND <additional observable constraint>
18
+ \`\`\`
19
+
20
+ ## Required Detail
21
+
22
+ Each criterion must identify:
23
+
24
+ - files or modules that are expected to change when known
25
+ - user-visible behavior or API behavior
26
+ - test expectation or verification command
27
+ - ownership when the work spans more than one wave or agent
28
+
29
+ ## Hard Blocks
30
+
31
+ - No loose checklist-only acceptance criteria.
32
+ - No vague outcomes such as "works correctly", "improves UX", or "handles errors" without observable behavior.
33
+ - No approval when criteria are missing \`GIVEN\`, \`WHEN\`, or \`THEN\`.
34
+ `;
35
+ }
36
+ export const planuBddCriteriaRule = {
37
+ id: 'planu-bdd-criteria',
38
+ name: 'Planu BDD Criteria',
39
+ description: 'Requires complete GIVEN/WHEN/THEN acceptance criteria.',
40
+ category: 'quality',
41
+ applicableHosts: ['all'],
42
+ defaultEnabled: true,
43
+ buildContent: (_host) => buildBody(),
44
+ };
45
+ //# sourceMappingURL=planu-bdd-criteria.js.map
@@ -0,0 +1,3 @@
1
+ import type { UniversalRule } from '../../../types/universal-rules/index.js';
2
+ export declare const planuEnglishSpecsRule: UniversalRule;
3
+ //# sourceMappingURL=planu-english-specs.d.ts.map