@planu/cli 4.11.1 → 4.11.3

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 (155) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/config/elicitation-questions.json +5 -17
  3. package/dist/engine/cascade-hooks/registry.js +0 -2
  4. package/dist/engine/challenge-scenarios-loader.d.ts +3 -3
  5. package/dist/engine/compliance-injector.d.ts +3 -3
  6. package/dist/engine/dashboard/kanban-logic.d.ts +1 -1
  7. package/dist/engine/doc-generator/portal/risk-matrix-svg.d.ts +1 -1
  8. package/dist/engine/doc-generator/proposal/proposal-generator.d.ts +1 -1
  9. package/dist/engine/elicitation/answer-extractor.d.ts +1 -4
  10. package/dist/engine/elicitation/answer-extractor.js +197 -68
  11. package/dist/engine/elicitation/decision-gap-detector.js +59 -79
  12. package/dist/engine/elicitation/index.d.ts +0 -1
  13. package/dist/engine/elicitation/index.js +0 -1
  14. package/dist/engine/elicitation/question-generator.js +10 -3
  15. package/dist/engine/elicitation/question-grounding-gate.js +83 -34
  16. package/dist/engine/evidence-gates/lifecycle-gate.js +27 -14
  17. package/dist/engine/evidence-index/index-builder.js +7 -2
  18. package/dist/engine/frontmatter-sha/frozen-fields.d.ts +1 -1
  19. package/dist/engine/handoff-artifacts/schemas/red-tests-handoff.d.ts +2 -2
  20. package/dist/engine/handoff-artifacts/schemas.d.ts +22 -22
  21. package/dist/engine/human-summary.js +3 -3
  22. package/dist/engine/implementation-contract/common.d.ts +1 -1
  23. package/dist/engine/implementation-contract/renderer.js +17 -39
  24. package/dist/engine/keyword-matcher/stopwords.d.ts +2 -2
  25. package/dist/engine/lifecycle-hints.d.ts +1 -1
  26. package/dist/engine/lifecycle-hints.js +13 -29
  27. package/dist/engine/provider-adapters/adapters/claude.d.ts +1 -1
  28. package/dist/engine/provider-adapters/adapters/gemini.d.ts +1 -1
  29. package/dist/engine/provider-adapters/adapters/gpt4.d.ts +1 -1
  30. package/dist/engine/provider-adapters/adapters/markdown.d.ts +1 -1
  31. package/dist/engine/safety/atomic-write-file.d.ts +1 -1
  32. package/dist/engine/safety/cross-process-lock.d.ts +1 -1
  33. package/dist/engine/safety/path-sanitizer.d.ts +1 -1
  34. package/dist/engine/security-analyzer/profile-detector.js +48 -16
  35. package/dist/engine/spec-format/copula-blacklist.d.ts +2 -2
  36. package/dist/engine/spec-format/lean-spec-generator.d.ts +2 -2
  37. package/dist/engine/spec-format/lean-spec-generator.js +9 -10
  38. package/dist/engine/spec-format/metadata-value-policy.d.ts +20 -20
  39. package/dist/engine/spec-generator/fallback-generator.js +13 -20
  40. package/dist/engine/spec-grounding/contract.d.ts +5 -0
  41. package/dist/engine/spec-grounding/contract.js +10 -6
  42. package/dist/engine/spec-migrator/lean-migration.js +9 -16
  43. package/dist/engine/text-signal-boundaries.js +45 -12
  44. package/dist/engine/universal-rules/rules/planu-modes.js +0 -2
  45. package/dist/engine/web-fetcher/stack-advisor.d.ts +2 -2
  46. package/dist/engine/web-fetcher/stack-advisor.js +78 -17
  47. package/dist/engine/well-known/manifest-schema.d.ts +33 -33
  48. package/dist/engine/workers/schema.d.ts +3 -3
  49. package/dist/hosts/claude-code/ux/mcp-resources.d.ts +4 -4
  50. package/dist/hosts/codex/rest-shim.d.ts +1 -1
  51. package/dist/native/lightweight-command-catalog.d.ts +30 -30
  52. package/dist/server/openapi-spec.d.ts +132 -132
  53. package/dist/tools/challenge-spec/challenge-report.d.ts +13 -0
  54. package/dist/tools/challenge-spec/challenge-report.js +55 -0
  55. package/dist/tools/challenge-spec/event-challenge-scenarios.js +8 -27
  56. package/dist/tools/challenge-spec/resilience-challenge-scenarios-a.js +16 -32
  57. package/dist/tools/challenge-spec/resilience-challenge-scenarios-b.js +15 -20
  58. package/dist/tools/challenge-spec/scenario-collector.d.ts +13 -0
  59. package/dist/tools/challenge-spec/scenario-collector.js +86 -0
  60. package/dist/tools/challenge-spec/scenarios-data.js +10 -9
  61. package/dist/tools/challenge-spec/scenarios-failure.js +38 -54
  62. package/dist/tools/challenge-spec/scenarios-scale.js +14 -15
  63. package/dist/tools/challenge-spec/scenarios-security.d.ts +1 -1
  64. package/dist/tools/challenge-spec/scenarios-security.js +18 -11
  65. package/dist/tools/challenge-spec/scenarios-utils.d.ts +31 -5
  66. package/dist/tools/challenge-spec/scenarios-utils.js +102 -6
  67. package/dist/tools/challenge-spec/security-challenge-scenarios.d.ts +1 -1
  68. package/dist/tools/challenge-spec/security-challenge-scenarios.js +20 -25
  69. package/dist/tools/challenge-spec.js +46 -124
  70. package/dist/tools/clarify-requirements/multiple-choice.js +1 -1
  71. package/dist/tools/clarify-requirements/questions-context.js +11 -11
  72. package/dist/tools/clarify-requirements/questions.js +3 -23
  73. package/dist/tools/clarify-requirements.js +38 -0
  74. package/dist/tools/compliance-gate-handler.d.ts +1 -1
  75. package/dist/tools/create-spec/autopilot-analyzer.d.ts +1 -1
  76. package/dist/tools/create-spec/autopilot-analyzer.js +29 -54
  77. package/dist/tools/create-spec/post-creation.js +23 -4
  78. package/dist/tools/create-spec.js +51 -168
  79. package/dist/tools/delete-first/suggest-deletions.d.ts +1 -1
  80. package/dist/tools/elicit-requirements-handler.js +20 -12
  81. package/dist/tools/generate-batch-script.d.ts +2 -2
  82. package/dist/tools/init-project/rules-generator.js +0 -1
  83. package/dist/tools/oauth-handler.d.ts +1 -1
  84. package/dist/tools/register-sdd-tools.d.ts +1 -1
  85. package/dist/tools/register-spec-tools/core-spec-tools.js +11 -2
  86. package/dist/tools/render-spec-for-provider.d.ts +2 -2
  87. package/dist/tools/scan-orphan-spec-refs.d.ts +6 -6
  88. package/dist/tools/schemas/agents.d.ts +7 -7
  89. package/dist/tools/schemas/analysis.d.ts +11 -11
  90. package/dist/tools/schemas/delete.d.ts +5 -5
  91. package/dist/tools/schemas/github.d.ts +4 -4
  92. package/dist/tools/schemas/ide-config.d.ts +12 -12
  93. package/dist/tools/schemas/infra.d.ts +9 -9
  94. package/dist/tools/schemas/lifecycle.d.ts +50 -50
  95. package/dist/tools/schemas/llm-provider-schemas.d.ts +7 -7
  96. package/dist/tools/schemas/plugins-schemas.d.ts +12 -12
  97. package/dist/tools/schemas/project-graph.d.ts +6 -6
  98. package/dist/tools/schemas/rbac.d.ts +2 -2
  99. package/dist/tools/schemas/registry.d.ts +3 -3
  100. package/dist/tools/schemas/runtime-security.d.ts +2 -2
  101. package/dist/tools/schemas/session.d.ts +2 -2
  102. package/dist/tools/schemas/spec.d.ts +15 -15
  103. package/dist/tools/schemas/token-intelligence.d.ts +6 -6
  104. package/dist/tools/schemas/token-optimization.d.ts +2 -2
  105. package/dist/tools/schemas/workers-schema.d.ts +4 -4
  106. package/dist/tools/skill-bootstrap-handler.d.ts +7 -7
  107. package/dist/tools/suggest-stack.js +49 -13
  108. package/dist/tools/tool-registry/deprecated-stubs.d.ts +1 -1
  109. package/dist/tools/triage-request.d.ts +3 -3
  110. package/dist/tools/update-status/dod-gates.d.ts +8 -0
  111. package/dist/tools/update-status/dod-gates.js +68 -43
  112. package/dist/tools/update-status/file-sync.d.ts +1 -1
  113. package/dist/tools/update-status/file-sync.js +10 -45
  114. package/dist/tools/update-status/index.js +133 -97
  115. package/dist/tools/update-status/qa-gate.js +26 -0
  116. package/dist/tools/update-status/response-builder.d.ts +1 -1
  117. package/dist/tools/update-status/response-builder.js +11 -19
  118. package/dist/tools/update-status/transition-guard.d.ts +2 -2
  119. package/dist/tools/update-status/transition-guard.js +11 -8
  120. package/dist/tools/update-status-actions.d.ts +0 -1
  121. package/dist/tools/update-status-actions.js +10 -26
  122. package/dist/tools/update-status-convention-gate.d.ts +1 -6
  123. package/dist/tools/update-status-convention-gate.js +9 -33
  124. package/dist/tools/validate-lint.d.ts +1 -1
  125. package/dist/tools/validate-lint.js +39 -1
  126. package/dist/types/docs.d.ts +0 -4
  127. package/dist/types/env.d.ts +0 -4
  128. package/dist/types/estimation.d.ts +0 -2
  129. package/dist/types/multi-teammate-review.d.ts +4 -4
  130. package/dist/types/plugin-install.d.ts +7 -7
  131. package/dist/types/spec/core.d.ts +20 -0
  132. package/dist/types/spec/inputs.d.ts +1 -1
  133. package/dist/types/stack/index.d.ts +1 -1
  134. package/dist/types/stack/recommend.d.ts +10 -0
  135. package/package.json +21 -18
  136. package/planu-native.json +1 -1
  137. package/planu-plugin.json +1 -1
  138. package/src/i18n/messages/en.json +0 -72
  139. package/src/i18n/messages/es.json +0 -72
  140. package/src/i18n/messages/pt.json +0 -72
  141. package/dist/config/elicitation-dimensions.json +0 -51
  142. package/dist/engine/actuals-estimator.d.ts +0 -14
  143. package/dist/engine/actuals-estimator.js +0 -64
  144. package/dist/engine/cascade-hooks/hooks/living-specs.hook.d.ts +0 -3
  145. package/dist/engine/cascade-hooks/hooks/living-specs.hook.js +0 -19
  146. package/dist/engine/elicitation/non-interactive-defaults.d.ts +0 -9
  147. package/dist/engine/elicitation/non-interactive-defaults.js +0 -28
  148. package/dist/engine/elicitation/option-builder.d.ts +0 -9
  149. package/dist/engine/elicitation/option-builder.js +0 -85
  150. package/dist/engine/progress-writer.d.ts +0 -10
  151. package/dist/engine/progress-writer.js +0 -63
  152. package/dist/tools/clarify-requirements/interview-mode.d.ts +0 -42
  153. package/dist/tools/clarify-requirements/interview-mode.js +0 -342
  154. package/dist/tools/update-status/mode-hints.d.ts +0 -4
  155. package/dist/tools/update-status/mode-hints.js +0 -24
@@ -1,15 +1,19 @@
1
+ import { extractCriteria } from '../spec-format/lean-spec-generator.js';
1
2
  export class FallbackGenerator {
2
3
  generate(request) {
3
- const sourceDescription = normalizeSourceDescription(request.description, request.title);
4
- const contextLine = buildContextLine(request);
4
+ const sourceDescription = normalizeSourceDescription(request.description);
5
+ const hasCriteria = extractCriteria(sourceDescription).length > 0;
5
6
  const technicalSection = '';
6
7
  const specBody = [
7
8
  '## Problem',
8
9
  sourceDescription,
9
- '',
10
- '## Solution',
11
- 'Use the preserved source description above as the authoritative requirements body.',
12
- contextLine,
10
+ ...(hasCriteria
11
+ ? []
12
+ : [
13
+ '',
14
+ '## Missing Decisions',
15
+ 'Needs decision: provide at least one explicit, verifiable acceptance criterion.',
16
+ ]),
13
17
  ].join('\n');
14
18
  return Promise.resolve({
15
19
  specBody,
@@ -19,28 +23,17 @@ export class FallbackGenerator {
19
23
  generatedAt: new Date().toISOString(),
20
24
  },
21
25
  qualityWarnings: [
22
- 'Fallback generator used. Source description was preserved; technical ownership must come from explicit source paths or local project analysis.',
26
+ 'Fallback generator used. Source description was preserved; missing decisions require explicit user input.',
23
27
  ],
24
28
  fallbackReason: 'No external model generator is configured for create_spec.',
25
29
  });
26
30
  }
27
31
  }
28
- function normalizeSourceDescription(description, title) {
32
+ function normalizeSourceDescription(description) {
29
33
  const compact = description.replace(/\r\n/g, '\n').trim();
30
34
  if (compact.length > 0) {
31
35
  return compact;
32
36
  }
33
- return `The project needs ${title} defined with enough implementation detail to proceed safely.`;
34
- }
35
- function buildContextLine(request) {
36
- const parts = [
37
- request.projectContext?.language,
38
- request.projectContext?.framework,
39
- request.projectContext?.architecture,
40
- ].filter((part) => part !== undefined && part.trim().length > 0);
41
- if (parts.length === 0) {
42
- return 'Use the existing project architecture and conventions.';
43
- }
44
- return `Use the existing ${parts.join(' / ')} project conventions.`;
37
+ return 'Needs decision: provide the source requirement description.';
45
38
  }
46
39
  //# sourceMappingURL=fallback-generator.js.map
@@ -6,6 +6,11 @@ export declare function filterGroundedCriteria(criteria: string[]): string[];
6
6
  export declare function buildCriterionGroundingRecords(args: {
7
7
  criteria: string[];
8
8
  userInput: string;
9
+ criterionEvidence?: {
10
+ criterion: string;
11
+ evidence: string[];
12
+ }[];
13
+ /** @deprecated Global evidence cannot ground a criterion. Use criterionEvidence. */
9
14
  projectEvidence?: string[];
10
15
  generatedEvidence?: string[];
11
16
  }): CriterionGroundingRecord[];
@@ -23,8 +23,11 @@ export function filterGroundedCriteria(criteria) {
23
23
  }
24
24
  export function buildCriterionGroundingRecords(args) {
25
25
  const userInput = normalizeCriterionText(args.userInput);
26
- const projectEvidence = args.projectEvidence ?? [];
27
26
  const generatedEvidence = args.generatedEvidence ?? [];
27
+ const criterionEvidence = new Map((args.criterionEvidence ?? []).map((record) => [
28
+ normalizeCriterionText(record.criterion),
29
+ record.evidence.filter((item) => item.trim().length > 0),
30
+ ]));
28
31
  return args.criteria.map((criterion) => {
29
32
  const normalized = normalizeCriterionText(criterion);
30
33
  if (isGenericCriterion(criterion)) {
@@ -43,12 +46,13 @@ export function buildCriterionGroundingRecords(args) {
43
46
  confidence: 'high',
44
47
  };
45
48
  }
46
- if (projectEvidence.length > 0) {
49
+ const directProjectEvidence = criterionEvidence.get(normalized) ?? [];
50
+ if (directProjectEvidence.length > 0) {
47
51
  return {
48
52
  text: criterion,
49
53
  source: 'project_evidence',
50
- evidence: projectEvidence.slice(0, 8),
51
- confidence: 'medium',
54
+ evidence: directProjectEvidence.slice(0, 8),
55
+ confidence: 'high',
52
56
  };
53
57
  }
54
58
  return {
@@ -108,7 +112,7 @@ export function checkGroundedSpecContract(spec, content) {
108
112
  issues.push(`Spec ${spec.id} requires grounding metadata but has no grounded criteria records.`);
109
113
  }
110
114
  for (const record of records) {
111
- if (record.source === 'ungrounded_advisory') {
115
+ if (record.source === 'ungrounded_advisory' || record.source === 'documented_assumption') {
112
116
  issues.push(`Criterion is advisory-only and cannot be approved as contract: ${record.text}`);
113
117
  }
114
118
  if (record.evidence.length === 0 && record.source !== 'ungrounded_advisory') {
@@ -117,7 +121,7 @@ export function checkGroundedSpecContract(spec, content) {
117
121
  }
118
122
  const technicalRecords = parseTechnicalReferenceGroundingRecords(frontmatter);
119
123
  for (const record of technicalRecords) {
120
- if (record.source === 'ungrounded_advisory') {
124
+ if (record.source === 'ungrounded_advisory' || record.source === 'documented_assumption') {
121
125
  issues.push(`Technical reference is advisory-only and cannot be contract: ${record.path}`);
122
126
  }
123
127
  if (record.evidence.length === 0 && record.source !== 'ungrounded_advisory') {
@@ -113,9 +113,6 @@ function extractOldCriteria(content) {
113
113
  }
114
114
  match = re.exec(content);
115
115
  }
116
- if (criteria.length === 0) {
117
- criteria.push({ text: 'Implementation complete and tested', done: false });
118
- }
119
116
  return criteria;
120
117
  }
121
118
  /** Check if a criterion is generic boilerplate (not specific to the feature). */
@@ -415,10 +412,14 @@ export async function migrateSpecToLean(specDir, projectPath) {
415
412
  const criteria = extractOldCriteria(specContent);
416
413
  const estimation = buildEstimationFromOld(metadata, oldTechContent);
417
414
  const spec = buildSpecFromMetadata(metadata, id, title, status, estimation, specPath, techPath);
418
- // Generate lean spec.md with extracted criteria
419
- const leanSpecLines = generateLeanSpecContent({ spec, description, estimation });
420
- // Inject real criteria (replace the default one)
421
- const leanSpecWithCriteria = replaceCriteria(leanSpecLines, criteria);
415
+ // Generate lean spec.md with extracted legacy criteria. Do not synthesize defaults:
416
+ // migrations preserve source-backed acceptance criteria only.
417
+ const leanSpecLines = generateLeanSpecContent({
418
+ spec,
419
+ description,
420
+ estimation,
421
+ criteriaOverride: criteria,
422
+ });
422
423
  // Generate the legacy technical body, then fold it into unified spec.md.
423
424
  const leanTech = generateLeanTechnicalContent({
424
425
  specId: id,
@@ -434,7 +435,7 @@ export async function migrateSpecToLean(specDir, projectPath) {
434
435
  return `backup failed: ${backupErr instanceof Error ? backupErr.message : String(backupErr)}`;
435
436
  }
436
437
  // Write one unified spec.md.
437
- await atomicWriteFile(specPath, buildUnifiedSpecContent(leanSpecWithCriteria, leanTech));
438
+ await atomicWriteFile(specPath, buildUnifiedSpecContent(leanSpecLines, leanTech));
438
439
  // Delete obsolete files
439
440
  for (const file of OBSOLETE_FILES) {
440
441
  const filePath = join(specDir, file);
@@ -457,14 +458,6 @@ export async function migrateSpecToLean(specDir, projectPath) {
457
458
  return err instanceof Error ? err.message : String(err);
458
459
  }
459
460
  }
460
- /** Replace criteria in lean spec content with extracted old criteria. */
461
- function replaceCriteria(content, criteria) {
462
- // Find the criteria section in YAML and replace it
463
- const criteriaLines = criteria.map((c) => ` - text: "${c.text.replace(/"/g, '\\"')}"\n done: ${String(c.done)}`);
464
- const newCriteria = `criteria:\n${criteriaLines.join('\n')}`;
465
- // Replace the existing criteria block
466
- return content.replace(/criteria:\n(?: {2}- text:.*\n {4}done:.*\n?)+/, `${newCriteria}\n`);
467
- }
468
461
  /** Migrate all specs in a project to lean format. */
469
462
  export async function migrateAllSpecsToLean(projectPath) {
470
463
  const result = { migratedCount: 0, skippedCount: 0, errors: [] };
@@ -1,5 +1,4 @@
1
1
  // engine/text-signal-boundaries.ts — Boundary-aware keyword matching for advisory signals.
2
- const NEGATION_WINDOW_WORDS = 5;
3
2
  const NEGATION_TERMS = new Set([
4
3
  'no',
5
4
  'not',
@@ -15,25 +14,57 @@ const NEGATION_TERMS = new Set([
15
14
  'never',
16
15
  ]);
17
16
  const OUT_OF_SCOPE_RE = /\b(?:out\s+of\s+scope|not\s+in\s+scope|no\s+scope|do\s+not|don't)\b/i;
17
+ const NON_AFFIRMING_CONTEXT_RE = /\b(?:false[ -]?positive|incorrect(?:ly)?|wrong(?:ly)?|irrelevant|unrelated|noise|repeated\s+failure)\b/i;
18
+ const EXCLUDED_SECTION_RE = /^(?:out\s+of\s+scope|non[ -]?goals?|examples?|anticipated\s+test\s+breaks?)\b/i;
19
+ function stripExcludedMarkdownSections(text) {
20
+ const lines = text.split('\n');
21
+ const kept = [];
22
+ let excludedLevel;
23
+ for (const line of lines) {
24
+ const heading = /^(#{1,6})\s+(.+?)\s*$/.exec(line);
25
+ if (heading) {
26
+ const marker = heading[1];
27
+ const title = heading[2];
28
+ if (marker === undefined || title === undefined) {
29
+ continue;
30
+ }
31
+ const level = marker.length;
32
+ if (excludedLevel !== undefined && level <= excludedLevel) {
33
+ excludedLevel = undefined;
34
+ }
35
+ if (EXCLUDED_SECTION_RE.test(title)) {
36
+ excludedLevel = level;
37
+ continue;
38
+ }
39
+ }
40
+ if (excludedLevel === undefined) {
41
+ kept.push(line);
42
+ }
43
+ }
44
+ return kept.join('\n');
45
+ }
18
46
  /** Remove fenced code and quoted examples before keyword-driven inference. */
19
47
  export function stripNonContractText(text) {
20
- return text
48
+ return stripExcludedMarkdownSections(text)
21
49
  .replace(/```[\s\S]*?```/g, ' ')
22
50
  .replace(/`[^`\n]+`/g, ' ')
23
51
  .replace(/"[^"\n]+"/g, ' ')
24
52
  .replace(/'[^'\n]+'/g, ' ');
25
53
  }
26
- function wordTokensBefore(text, index) {
27
- return text
28
- .slice(Math.max(0, index - 160), index)
29
- .toLowerCase()
30
- .split(/[^a-z0-9]+/i)
31
- .filter(Boolean);
54
+ function clauseBefore(text, index) {
55
+ const before = text.slice(Math.max(0, index - 320), index);
56
+ const boundary = Math.max(before.lastIndexOf('\n'), before.lastIndexOf('.'), before.lastIndexOf(';'));
57
+ return before.slice(boundary + 1);
32
58
  }
33
59
  function isNegatedAt(text, index) {
34
- const before = wordTokensBefore(text, index);
35
- const recent = before.slice(-NEGATION_WINDOW_WORDS);
36
- return recent.some((word) => NEGATION_TERMS.has(word));
60
+ const before = clauseBefore(text, index)
61
+ .replace(/\bnot\s+only\b/gi, '')
62
+ .toLowerCase();
63
+ const tokens = before.split(/[^a-z0-9]+/i).filter(Boolean);
64
+ return tokens.some((word) => NEGATION_TERMS.has(word));
65
+ }
66
+ function isNonAffirmingAt(text, index) {
67
+ return NON_AFFIRMING_CONTEXT_RE.test(clauseBefore(text, index));
37
68
  }
38
69
  function isOutOfScopeLine(line, indexInLine) {
39
70
  return OUT_OF_SCOPE_RE.test(line.slice(0, Math.max(indexInLine, 0) + 80));
@@ -49,7 +80,9 @@ export function hasAffirmedMatch(text, pattern) {
49
80
  const lineStart = cleaned.lastIndexOf('\n', index) + 1;
50
81
  const lineEnd = cleaned.indexOf('\n', index);
51
82
  const line = cleaned.slice(lineStart, lineEnd === -1 ? cleaned.length : lineEnd);
52
- if (!isNegatedAt(cleaned, index) && !isOutOfScopeLine(line, index - lineStart)) {
83
+ if (!isNegatedAt(cleaned, index) &&
84
+ !isNonAffirmingAt(cleaned, index) &&
85
+ !isOutOfScopeLine(line, index - lineStart)) {
53
86
  return true;
54
87
  }
55
88
  match = re.exec(cleaned);
@@ -21,7 +21,6 @@ Never suggest bypassPermissions — it disables all safety checks.
21
21
 
22
22
  ## How to apply
23
23
 
24
- - After \`update_status\` returns \`suggestedClaudeMode\`, relay it to the user: "I recommend switching to \`/mode <mode>\`"
25
24
  - NEVER switch modes automatically — always tell the user and let them decide
26
25
  - Critical bash actions (git push, npm publish, rm -rf) require approval regardless of mode
27
26
  `;
@@ -43,7 +42,6 @@ Auto-generated by \`init_project\`.
43
42
 
44
43
  ## How to apply
45
44
 
46
- - After \`update_status\` completes, suggest the appropriate mode to the user
47
45
  - NEVER switch modes automatically — always inform the user first
48
46
  `;
49
47
  }
@@ -1,4 +1,4 @@
1
- import type { StackRecommendation, SuggestStackResult } from '../../types/index.js';
1
+ import type { StackAdviceGrounding, StackRecommendation, SuggestStackResult } from '../../types/index.js';
2
2
  export declare function verifyFrameworkVersion(framework: string): Promise<string | null>;
3
3
  export declare function recommendStack(projectType: string, requirements: string[], constraints: string[]): StackRecommendation;
4
4
  export declare function recommendAlternativeStack(projectType: string, _requirements: string[]): StackRecommendation;
@@ -11,5 +11,5 @@ export declare function buildMcpCompatibility(stack: StackRecommendation): {
11
11
  compatible: boolean;
12
12
  notes: string;
13
13
  }[];
14
- export declare function buildSuggestStackResult(projectType: string, requirements: string[], constraints: string[]): Promise<SuggestStackResult>;
14
+ export declare function buildSuggestStackResult(projectType: string, requirements: string[], constraints: string[]): Promise<SuggestStackResult & StackAdviceGrounding>;
15
15
  //# sourceMappingURL=stack-advisor.d.ts.map
@@ -96,20 +96,24 @@ function getBaseRecommendations() {
96
96
  },
97
97
  };
98
98
  }
99
- function applyRequirementOverrides(rec, reqStr, constStr) {
99
+ function applyRequirementOverrides(rec, requirements, constraints) {
100
100
  let result = rec;
101
- if (reqStr.includes('python') || reqStr.includes('ml') || reqStr.includes('ai')) {
101
+ if (hasAffirmedTerm(requirements, /\bpython\b/i) ||
102
+ hasAffirmedTerm(requirements, /\bml\b/i) ||
103
+ hasAffirmedTerm(requirements, /\bai\b/i)) {
104
+ const requestsApi = hasAffirmedTerm(requirements, /\bapi\b/i);
102
105
  result = {
103
106
  ...result,
104
107
  language: 'python',
105
- framework: reqStr.includes('api') ? 'fastapi' : 'django',
106
- version: reqStr.includes('api') ? '0.100+' : '5+',
107
- ecosystem: reqStr.includes('api')
108
+ framework: requestsApi ? 'fastapi' : 'django',
109
+ version: requestsApi ? '0.100+' : '5+',
110
+ ecosystem: requestsApi
108
111
  ? ['fastapi', 'pydantic', 'sqlalchemy', 'alembic']
109
112
  : ['django', 'django-rest-framework', 'celery', 'pytest'],
110
113
  };
111
114
  }
112
- if (reqStr.includes('go') || reqStr.includes('golang') || reqStr.includes('performance')) {
115
+ if (hasAffirmedTerm(requirements, /\bgo(?:lang)?\b/i) ||
116
+ hasAffirmedTerm(requirements, /\bperformance\b/i)) {
113
117
  result = {
114
118
  ...result,
115
119
  language: 'go',
@@ -118,7 +122,7 @@ function applyRequirementOverrides(rec, reqStr, constStr) {
118
122
  ecosystem: ['gin', 'gorm', 'wire', 'testify'],
119
123
  };
120
124
  }
121
- if (reqStr.includes('supabase')) {
125
+ if (hasAffirmedTerm(requirements, /\bsupabase\b/i)) {
122
126
  result = {
123
127
  ...result,
124
128
  database: 'supabase',
@@ -126,22 +130,21 @@ function applyRequirementOverrides(rec, reqStr, constStr) {
126
130
  auth: 'supabase-auth',
127
131
  };
128
132
  }
129
- if (reqStr.includes('firebase')) {
133
+ if (hasAffirmedTerm(requirements, /\bfirebase\b/i)) {
130
134
  result = { ...result, database: 'firebase', auth: 'firebase-auth' };
131
135
  }
132
- if (constStr.includes('no-vendor-lock') || constStr.includes('self-host')) {
136
+ if (constraints.some((constraint) => constraint.toLowerCase().includes('no-vendor-lock')) ||
137
+ hasAffirmedTerm(constraints, /\bself[- ]host(?:ed|ing)?\b/i)) {
133
138
  result = { ...result, deployment: 'docker + railway', database: 'postgresql' };
134
139
  }
135
140
  return result;
136
141
  }
137
142
  export function recommendStack(projectType, requirements, constraints) {
138
- const reqStr = requirements.join(' ').toLowerCase();
139
- const constStr = constraints.join(' ').toLowerCase();
140
143
  const recommendations = getBaseRecommendations();
141
144
  const base =
142
145
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- 'web' key always exists in getBaseRecommendations()
143
146
  recommendations[projectType] ?? recommendations.web;
144
- return applyRequirementOverrides(base, reqStr, constStr);
147
+ return applyRequirementOverrides(base, requirements, constraints);
145
148
  }
146
149
  export function recommendAlternativeStack(projectType, _requirements) {
147
150
  const alternatives = {
@@ -208,23 +211,23 @@ export function buildWhyNotOthers(_projectType, chosenFramework) {
208
211
  const allAlternatives = [
209
212
  {
210
213
  name: 'Ruby on Rails',
211
- reason: 'Great for prototyping but TypeScript ecosystem is larger and has better type safety.',
214
+ reason: 'Optional candidate; compare it against the confirmed requirements before selection.',
212
215
  },
213
216
  {
214
217
  name: 'PHP/Laravel',
215
- reason: 'Solid framework but less modern tooling and TypeScript interop.',
218
+ reason: 'Optional candidate; compare it against the confirmed requirements before selection.',
216
219
  },
217
220
  {
218
221
  name: 'Java/Spring',
219
- reason: 'Enterprise-grade but heavier setup and verbose for smaller projects.',
222
+ reason: 'Optional candidate; compare it against the confirmed requirements before selection.',
220
223
  },
221
224
  {
222
225
  name: 'C#/.NET',
223
- reason: 'Excellent for enterprise but tighter coupling to Microsoft ecosystem.',
226
+ reason: 'Optional candidate; compare it against the confirmed requirements before selection.',
224
227
  },
225
228
  {
226
229
  name: 'Elixir/Phoenix',
227
- reason: 'Great for real-time but smaller ecosystem and talent pool.',
230
+ reason: 'Optional candidate; compare it against the confirmed requirements before selection.',
228
231
  },
229
232
  ];
230
233
  return allAlternatives
@@ -307,6 +310,8 @@ export async function buildSuggestStackResult(projectType, requirements, constra
307
310
  // Offline: keep static versions, mark confidence as low
308
311
  confidence = 'low';
309
312
  }
313
+ const detectedEvidence = collectDetectedEvidence(requirements, constraints);
314
+ const confirmedChoices = collectConfirmedChoices(requirements);
310
315
  return {
311
316
  primary,
312
317
  alternative,
@@ -314,6 +319,62 @@ export async function buildSuggestStackResult(projectType, requirements, constra
314
319
  mcpCompatibility: buildMcpCompatibility(primary),
315
320
  lastChecked: new Date().toISOString(),
316
321
  confidence,
322
+ selectionStatus: confirmedChoices.length > 0 ? 'partially-confirmed' : 'unconfirmed',
323
+ detectedEvidence,
324
+ confirmedChoices,
325
+ candidateNotice: 'Recommendations are optional candidates. No candidate is selected or persisted without user confirmation.',
317
326
  };
318
327
  }
328
+ const NEGATION_RE = /\b(?:no|not|never|without|exclude|excluding|forbid|forbidden|avoid|must not|should not|do not|does not|don't|doesn't|out[- ]of[- ]scope)\b/i;
329
+ const NEGATIVE_SUFFIX_RE = /\b(?:forbidden|excluded|not allowed|out[- ]of[- ]scope)\b/i;
330
+ function hasAffirmedTerm(values, pattern) {
331
+ return values.some((value) => {
332
+ const withoutQuotedText = value
333
+ .replace(/```[\s\S]*?```/g, ' ')
334
+ .replace(/`[^`]+`/g, ' ')
335
+ .replace(/"[^"]+"|'[^']+'/g, ' ');
336
+ return withoutQuotedText.split(/(?:\r?\n|[.,;!?]+)/).some((segment) => {
337
+ const match = new RegExp(pattern.source, pattern.flags.replace('g', '')).exec(segment);
338
+ if (!match) {
339
+ return false;
340
+ }
341
+ const prefix = segment.slice(0, match.index);
342
+ const suffix = segment.slice(match.index + match[0].length);
343
+ return !NEGATION_RE.test(prefix) && !NEGATIVE_SUFFIX_RE.test(suffix);
344
+ });
345
+ });
346
+ }
347
+ function collectDetectedEvidence(requirements, constraints) {
348
+ const definitions = [
349
+ ['language:python', /\bpython\b/i],
350
+ ['language:go', /\bgo(?:lang)?\b/i],
351
+ ['database:supabase', /\bsupabase\b/i],
352
+ ['database:firebase', /\bfirebase\b/i],
353
+ ['capability:api', /\bapi\b/i],
354
+ ['capability:ml', /\b(?:ml|ai)\b/i],
355
+ ];
356
+ const evidence = definitions
357
+ .filter(([, pattern]) => hasAffirmedTerm(requirements, pattern))
358
+ .map(([name]) => `requirement:${name}`);
359
+ if (constraints.some((constraint) => constraint.toLowerCase().includes('no-vendor-lock'))) {
360
+ evidence.push('constraint:no-vendor-lock');
361
+ }
362
+ if (hasAffirmedTerm(constraints, /\bself[- ]host(?:ed|ing)?\b/i)) {
363
+ evidence.push('constraint:self-host');
364
+ }
365
+ return evidence;
366
+ }
367
+ function collectConfirmedChoices(requirements) {
368
+ const confirmationText = requirements.join(' ');
369
+ const choices = [
370
+ ['language', 'python', /\b(?:must use|use|selected|confirmed|requires?)\s+python\b/i],
371
+ ['language', 'go', /\b(?:must use|use|selected|confirmed|requires?)\s+go(?:lang)?\b/i],
372
+ ['database', 'supabase', /\b(?:must use|use|selected|confirmed|requires?)\s+supabase\b/i],
373
+ ['database', 'firebase', /\b(?:must use|use|selected|confirmed|requires?)\s+firebase\b/i],
374
+ ];
375
+ return choices
376
+ .filter(([, , pattern]) => pattern.test(confirmationText))
377
+ .filter(([, value]) => hasAffirmedTerm(requirements, new RegExp(`\\b${value}\\b`, 'i')))
378
+ .map(([field, value]) => ({ field, value, source: 'user_confirmation' }));
379
+ }
319
380
  //# sourceMappingURL=stack-advisor.js.map
@@ -6,87 +6,87 @@ export type { KnownCapability };
6
6
  * RFC 8615 .well-known URI pattern — follows OpenID Connect / OAuth conventions.
7
7
  */
8
8
  export declare const MANIFEST_SCHEMA_V1: {
9
- readonly $schema: "http://json-schema.org/draft-07/schema#";
10
- readonly $id: "https://planu.dev/schemas/well-known-manifest-v1.json";
11
- readonly title: "Planu .well-known Manifest v1";
12
- readonly description: "Spec registry discovery manifest published at .well-known/planu.json";
13
- readonly type: "object";
14
- readonly required: readonly ["version", "name", "specCount", "specs", "capabilities", "generatedAt"];
9
+ readonly $schema: 'http://json-schema.org/draft-07/schema#';
10
+ readonly $id: 'https://planu.dev/schemas/well-known-manifest-v1.json';
11
+ readonly title: 'Planu .well-known Manifest v1';
12
+ readonly description: 'Spec registry discovery manifest published at .well-known/planu.json';
13
+ readonly type: 'object';
14
+ readonly required: readonly ['version', 'name', 'specCount', 'specs', 'capabilities', 'generatedAt'];
15
15
  readonly additionalProperties: false;
16
16
  readonly properties: {
17
17
  readonly version: {
18
- readonly type: "number";
18
+ readonly type: 'number';
19
19
  readonly const: 1;
20
- readonly description: "Manifest schema version. Always 1 for this format.";
20
+ readonly description: 'Manifest schema version. Always 1 for this format.';
21
21
  };
22
22
  readonly name: {
23
- readonly type: "string";
23
+ readonly type: 'string';
24
24
  readonly minLength: 1;
25
25
  readonly maxLength: 200;
26
- readonly description: "Project display name.";
26
+ readonly description: 'Project display name.';
27
27
  };
28
28
  readonly specCount: {
29
- readonly type: "integer";
29
+ readonly type: 'integer';
30
30
  readonly minimum: 0;
31
- readonly description: "Total number of specs included in this manifest.";
31
+ readonly description: 'Total number of specs included in this manifest.';
32
32
  };
33
33
  readonly specs: {
34
- readonly type: "array";
35
- readonly description: "Array of spec summaries.";
34
+ readonly type: 'array';
35
+ readonly description: 'Array of spec summaries.';
36
36
  readonly items: {
37
- readonly type: "object";
38
- readonly required: readonly ["id", "title", "status", "type", "description", "tags", "created"];
37
+ readonly type: 'object';
38
+ readonly required: readonly ['id', 'title', 'status', 'type', 'description', 'tags', 'created'];
39
39
  readonly additionalProperties: false;
40
40
  readonly properties: {
41
41
  readonly id: {
42
- readonly type: "string";
43
- readonly pattern: "^SPEC-\\d+$";
42
+ readonly type: 'string';
43
+ readonly pattern: '^SPEC-\\d+$';
44
44
  };
45
45
  readonly title: {
46
- readonly type: "string";
46
+ readonly type: 'string';
47
47
  readonly maxLength: 300;
48
48
  };
49
49
  readonly status: {
50
- readonly type: "string";
50
+ readonly type: 'string';
51
51
  readonly maxLength: 50;
52
52
  };
53
53
  readonly type: {
54
- readonly type: "string";
54
+ readonly type: 'string';
55
55
  readonly maxLength: 50;
56
56
  };
57
57
  readonly description: {
58
- readonly type: "string";
58
+ readonly type: 'string';
59
59
  readonly maxLength: 200;
60
60
  };
61
61
  readonly tags: {
62
- readonly type: "array";
62
+ readonly type: 'array';
63
63
  readonly items: {
64
- readonly type: "string";
64
+ readonly type: 'string';
65
65
  readonly maxLength: 100;
66
66
  };
67
67
  };
68
68
  readonly created: {
69
- readonly type: "string";
70
- readonly format: "date";
69
+ readonly type: 'string';
70
+ readonly format: 'date';
71
71
  };
72
72
  };
73
73
  };
74
74
  };
75
75
  readonly capabilities: {
76
- readonly type: "array";
77
- readonly description: "Planu capabilities exported by this project.";
76
+ readonly type: 'array';
77
+ readonly description: 'Planu capabilities exported by this project.';
78
78
  readonly items: {
79
- readonly type: "string";
79
+ readonly type: 'string';
80
80
  readonly maxLength: 100;
81
81
  };
82
82
  };
83
83
  readonly generatedAt: {
84
- readonly type: "string";
85
- readonly format: "date-time";
86
- readonly description: "ISO 8601 timestamp when this manifest was generated.";
84
+ readonly type: 'string';
85
+ readonly format: 'date-time';
86
+ readonly description: 'ISO 8601 timestamp when this manifest was generated.';
87
87
  };
88
88
  };
89
89
  };
90
90
  /** Known capabilities that a project can declare in its manifest. */
91
- export declare const KNOWN_CAPABILITIES: readonly ["spec-registry", "federation", "living-spec", "ai-analysis", "estimation", "changelog"];
91
+ export declare const KNOWN_CAPABILITIES: readonly ['spec-registry', 'federation', 'living-spec', 'ai-analysis', 'estimation', 'changelog'];
92
92
  //# sourceMappingURL=manifest-schema.d.ts.map
@@ -2,8 +2,8 @@ import { z } from 'zod';
2
2
  export declare const WorkerTriggerSchema: z.ZodObject<{
3
3
  type: z.ZodEnum<{
4
4
  event: "event";
5
- schedule: "schedule";
6
5
  "file-change": "file-change";
6
+ schedule: "schedule";
7
7
  }>;
8
8
  patterns: z.ZodOptional<z.ZodArray<z.ZodString>>;
9
9
  events: z.ZodOptional<z.ZodArray<z.ZodString>>;
@@ -15,8 +15,8 @@ export declare const WorkerDefinitionSchema: z.ZodObject<{
15
15
  trigger: z.ZodObject<{
16
16
  type: z.ZodEnum<{
17
17
  event: "event";
18
- schedule: "schedule";
19
18
  "file-change": "file-change";
19
+ schedule: "schedule";
20
20
  }>;
21
21
  patterns: z.ZodOptional<z.ZodArray<z.ZodString>>;
22
22
  events: z.ZodOptional<z.ZodArray<z.ZodString>>;
@@ -38,8 +38,8 @@ export declare const WorkerRegistrySchema: z.ZodObject<{
38
38
  trigger: z.ZodObject<{
39
39
  type: z.ZodEnum<{
40
40
  event: "event";
41
- schedule: "schedule";
42
41
  "file-change": "file-change";
42
+ schedule: "schedule";
43
43
  }>;
44
44
  patterns: z.ZodOptional<z.ZodArray<z.ZodString>>;
45
45
  events: z.ZodOptional<z.ZodArray<z.ZodString>>;
@@ -2,10 +2,10 @@ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import type { McpResourceMeta } from '../../../types/claude-code-ux.js';
3
3
  /** Planu-namespaced resource URIs exposed by SPEC-588. */
4
4
  export declare const PLANU_UX_RESOURCE_URIS: {
5
- readonly specList: "planu://ux/specs";
6
- readonly progress: "planu://ux/progress";
7
- readonly constitution: "planu://ux/constitution";
8
- readonly recentActivity: "planu://ux/recent-activity";
5
+ readonly specList: 'planu://ux/specs';
6
+ readonly progress: 'planu://ux/progress';
7
+ readonly constitution: 'planu://ux/constitution';
8
+ readonly recentActivity: 'planu://ux/recent-activity';
9
9
  };
10
10
  export declare const specListResourceMeta: McpResourceMeta;
11
11
  export declare const progressResourceMeta: McpResourceMeta;
@@ -1,3 +1,3 @@
1
- export declare const REST_SHIM_STATUS: "disabled";
1
+ export declare const REST_SHIM_STATUS: 'disabled';
2
2
  export declare const REST_SHIM_REASON = "Codex supports MCP-over-stdio natively. REST shim is not needed.";
3
3
  //# sourceMappingURL=rest-shim.d.ts.map