@planu/cli 5.7.5 → 5.7.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +88 -0
- package/README.md +3 -3
- package/dist/.planu-build.json +1 -1
- package/dist/cli/commands/package-handoff.js +30 -5
- package/dist/config/compliance-profiles.json +36 -3
- package/dist/config/environment-schema.json +21 -0
- package/dist/config/hook-templates/planu-review-panel-trigger.sh +2 -3
- package/dist/config/hook-templates/planu-session-safeguard.sh +22 -15
- package/dist/config/official-sdd-tools.d.ts +1 -1
- package/dist/config/official-sdd-tools.js +11 -24
- package/dist/config/registries/hosts/codex.json +14 -30
- package/dist/config/registries/hosts/opencode.json +1 -6
- package/dist/config/server-instructions.js +0 -21
- package/dist/config/skill-templates/planu-context-assets.md +3 -6
- package/dist/config/skill-templates/planu-implement.md +2 -2
- package/dist/config/skill-templates/planu-multi-teammate-review.md +8 -12
- package/dist/config/skill-templates/planu-release.md +2 -7
- package/dist/config/skill-templates/planu-validate.md +3 -3
- package/dist/config/spec-templates/crud-rest-api/template.json +5 -1
- package/dist/config/spec-templates/file-upload-s3/template.json +5 -1
- package/dist/config/spec-templates/stripe-payments/template.json +5 -1
- package/dist/config/spec-templates/webhook-system/template.json +5 -1
- package/dist/config/subagent-templates/planu-challenger.md +4 -9
- package/dist/config/subagent-templates/planu-spec-implementer.md +2 -4
- package/dist/config/subagent-templates/planu-validator.md +5 -8
- package/dist/config/tool-groups.json +6 -144
- package/dist/engine/ambiguity-scorer.js +40 -1
- package/dist/engine/autopilot/bootstrap.js +1 -1
- package/dist/engine/code-impact-analyzer.js +2 -6
- package/dist/engine/context-orchestrator/index.js +2 -2
- package/dist/engine/convention-scanner/codebase-scanner.js +2 -2
- package/dist/engine/dogfooding/runtime-gap-detector.js +2 -2
- package/dist/engine/evidence-gates/artifact-reader.d.ts +1 -0
- package/dist/engine/evidence-gates/artifact-reader.js +1 -1
- package/dist/engine/evidence-gates/evidence-autofill.d.ts +13 -5
- package/dist/engine/evidence-gates/evidence-autofill.js +106 -43
- package/dist/engine/evidence-gates/evidence-skeletons.js +3 -1
- package/dist/engine/evidence-gates/lifecycle-gate.js +7 -4
- package/dist/engine/evidence-index/index-builder.js +21 -4
- package/dist/engine/execution/job-runtime.js +1 -1
- package/dist/engine/execution/package-handoff-job-executor.d.ts +22 -0
- package/dist/engine/execution/package-handoff-job-executor.js +490 -0
- package/dist/engine/execution/validate-job-executor.js +20 -8
- package/dist/engine/git/exec-git.d.ts +4 -0
- package/dist/engine/git/exec-git.js +14 -0
- package/dist/engine/handoff-artifacts/implementation-review-reader.js +15 -2
- package/dist/engine/handoff-artifacts/schemas.d.ts +2 -0
- package/dist/engine/handoff-artifacts/schemas.js +1 -0
- package/dist/engine/handoff-format.d.ts +36 -0
- package/dist/engine/handoff-format.js +314 -0
- package/dist/engine/handoff-packager.d.ts +5 -3
- package/dist/engine/handoff-packager.js +89 -32
- package/dist/engine/host-tool-filter.js +20 -49
- package/dist/engine/human-summary.js +14 -3
- package/dist/engine/implementation-contract/common.d.ts +8 -3
- package/dist/engine/implementation-contract/common.js +9 -6
- package/dist/engine/implementation-contract/evaluator.js +4 -6
- package/dist/engine/implementation-contract/renderer.js +18 -23
- package/dist/engine/project-health-checker.js +2 -2
- package/dist/engine/readiness-checker.js +43 -5
- package/dist/engine/reconcile/apply-changes.js +4 -0
- package/dist/engine/reconcile/propagate-mirrors.d.ts +3 -0
- package/dist/engine/reconcile/propagate-mirrors.js +311 -0
- package/dist/engine/self-healing/healer.js +12 -5
- package/dist/engine/self-healing/strategies/lint-fix.d.ts +1 -1
- package/dist/engine/self-healing/strategies/lint-fix.js +4 -1
- package/dist/engine/self-healing/strategies/test-fix.d.ts +4 -3
- package/dist/engine/self-healing/strategies/test-fix.js +32 -14
- package/dist/engine/self-healing/strategies/typescript-fix.d.ts +1 -1
- package/dist/engine/self-healing/strategies/typescript-fix.js +28 -10
- package/dist/engine/session/checkpoint-writer.js +8 -4
- package/dist/engine/session/session-tracker.d.ts +5 -16
- package/dist/engine/session/session-tracker.js +29 -26
- package/dist/engine/session-state/writer.js +3 -11
- package/dist/engine/skill-generator/workflow-skill-generator.d.ts +4 -2
- package/dist/engine/skill-generator/workflow-skill-generator.js +113 -38
- package/dist/engine/spec-format/bdd-parser.d.ts +1 -11
- package/dist/engine/spec-format/bdd-parser.js +40 -11
- package/dist/engine/spec-format/lean-spec-generator.js +32 -12
- package/dist/engine/spec-format/retired-scaffold.d.ts +3 -0
- package/dist/engine/spec-format/retired-scaffold.js +5 -0
- package/dist/engine/spec-format/technical-md-populator.d.ts +1 -0
- package/dist/engine/spec-format/technical-md-populator.js +27 -0
- package/dist/engine/spec-format/unified-spec-builder.d.ts +1 -0
- package/dist/engine/spec-format/unified-spec-builder.js +30 -20
- package/dist/engine/spec-grounding/contract.d.ts +3 -3
- package/dist/engine/spec-grounding/contract.js +65 -4
- package/dist/engine/spec-grounding/grounding-entry-parser.d.ts +5 -0
- package/dist/engine/spec-grounding/grounding-entry-parser.js +185 -0
- package/dist/engine/spec-quality/generic-output-gate.js +37 -3
- package/dist/engine/technical-enricher/index.js +4 -1
- package/dist/engine/type-safety-gate.js +3 -11
- package/dist/engine/universal-rules/catalog.js +0 -2
- package/dist/engine/universal-rules/rules/planu-workflow.js +1 -1
- package/dist/engine/validation/durable-validation.d.ts +3 -2
- package/dist/engine/validation/durable-validation.js +106 -40
- package/dist/engine/validation/validation-freshness.d.ts +3 -2
- package/dist/engine/validation/validation-freshness.js +22 -16
- package/dist/engine/validation/validation-worktree.js +34 -0
- package/dist/hosts/claude-code/ux/mcp-prompts.js +8 -8
- package/dist/index.js +3 -17
- package/dist/resources/process.js +38 -65
- package/dist/storage/session-state-store.js +2 -0
- package/dist/storage/spec-store.js +8 -1
- package/dist/tools/bump-spec-version.js +5 -5
- package/dist/tools/challenge-spec/scenarios-utils.js +2 -2
- package/dist/tools/configure-checkpoint-policy.js +3 -3
- package/dist/tools/create-spec-helpers.js +2 -5
- package/dist/tools/create-spec.js +21 -4
- package/dist/tools/facilitate.js +3 -6
- package/dist/tools/git/branch-ops.js +16 -4
- package/dist/tools/github-release-handler.js +1 -20
- package/dist/tools/init-project/agents-md-writer.js +6 -6
- package/dist/tools/init-project/claude-md-generator.d.ts +0 -1
- package/dist/tools/init-project/claude-md-generator.js +0 -35
- package/dist/tools/init-project/handler.js +7 -5
- package/dist/tools/init-project/per-client-files-writer.js +5 -5
- package/dist/tools/init-project/planu-workflow-generator.js +2 -13
- package/dist/tools/init-project/portable-index-reconciler.d.ts +2 -1
- package/dist/tools/init-project/portable-index-reconciler.js +7 -2
- package/dist/tools/init-project/rules-generator.d.ts +0 -11
- package/dist/tools/init-project/rules-generator.js +4 -85
- package/dist/tools/init-project/scaffold-writer.d.ts +0 -1
- package/dist/tools/init-project/scaffold-writer.js +1 -12
- package/dist/tools/init-project/skills-multi-teammate-review-writer.js +1 -1
- package/dist/tools/jobs/handlers.d.ts +0 -6
- package/dist/tools/jobs/handlers.js +13 -40
- package/dist/tools/list-specs.js +6 -21
- package/dist/tools/multi-teammate-review.js +1 -14
- package/dist/tools/package-handoff.d.ts +3 -2
- package/dist/tools/package-handoff.js +153 -427
- package/dist/tools/reconcile-session-safeguard-hook.js +22 -15
- package/dist/tools/register-platform-tools/design-stack-tools.js +3 -3
- package/dist/tools/register-sdd-tools.js +0 -23
- package/dist/tools/register-spec-tools/analysis-tools.js +22 -9
- package/dist/tools/register-spec-tools/core-spec-tools.js +115 -70
- package/dist/tools/schemas/index.d.ts +1 -1
- package/dist/tools/schemas/index.js +1 -1
- package/dist/tools/schemas/output-schemas.d.ts +1 -18
- package/dist/tools/schemas/output-schemas.js +1 -11
- package/dist/tools/schemas/package-handoff-output-schema.d.ts +40 -0
- package/dist/tools/schemas/package-handoff-output-schema.js +68 -0
- package/dist/tools/schemas/validate-output-schema.d.ts +1 -2
- package/dist/tools/schemas/validate-output-schema.js +3 -2
- package/dist/tools/session-checkpoint.js +2 -2
- package/dist/tools/skill-registry/index.d.ts +0 -1
- package/dist/tools/skill-registry/index.js +0 -1
- package/dist/tools/sync-spec-state-handler.d.ts +2 -2
- package/dist/tools/sync-spec-state-handler.js +23 -3
- package/dist/tools/tool-registry/core-tools.js +72 -164
- package/dist/tools/tool-registry/group-infra.js +2 -39
- package/dist/tools/tool-registry/group-quality-compliance.js +20 -15
- package/dist/tools/tool-registry-helpers.js +6 -2
- package/dist/tools/update-status/batch.js +17 -0
- package/dist/tools/update-status/dod-gates.js +14 -1
- package/dist/tools/update-status/done-receipt-verifier.d.ts +22 -0
- package/dist/tools/update-status/done-receipt-verifier.js +56 -26
- package/dist/tools/update-status/evidence-gate.js +10 -21
- package/dist/tools/update-status/file-sync.d.ts +1 -0
- package/dist/tools/update-status/file-sync.js +6 -0
- package/dist/tools/update-status/index.d.ts +6 -0
- package/dist/tools/update-status/index.js +39 -44
- package/dist/tools/update-status-actions.js +2 -7
- package/dist/tools/validate.js +34 -28
- package/dist/transports/oauth-validator.js +12 -1
- package/dist/transports/transport-factory.d.ts +2 -1
- package/dist/transports/transport-factory.js +19 -0
- package/dist/types/common/primitives.d.ts +2 -0
- package/dist/types/durable-job.d.ts +45 -0
- package/dist/types/durable-validation.d.ts +1 -1
- package/dist/types/evidence-autofill.d.ts +13 -1
- package/dist/types/handoff-artifacts.d.ts +2 -1
- package/dist/types/readiness.d.ts +8 -1
- package/dist/types/reconcile.d.ts +6 -0
- package/dist/types/skill-registry.d.ts +26 -1
- package/dist/types/spec/core.d.ts +5 -0
- package/dist/types/spec/inputs.d.ts +2 -2
- package/dist/types/spec-format.d.ts +11 -1
- package/dist/types/spec-grounding.d.ts +26 -0
- package/dist/types/transport.d.ts +1 -0
- package/dist/types/validation-receipt.d.ts +27 -0
- package/package.json +3 -2
- package/planu-plugin.json +13 -26
- package/src/i18n/messages/en.json +1 -1
- package/src/i18n/messages/es.json +1 -1
- package/src/i18n/messages/pt.json +1 -1
- package/dist/engine/skill-generator/conventions-hasher.d.ts +0 -7
- package/dist/engine/skill-generator/conventions-hasher.js +0 -24
- package/dist/engine/universal-rules/rules/agent-teams.d.ts +0 -3
- package/dist/engine/universal-rules/rules/agent-teams.js +0 -63
- package/dist/tools/clarify-requirements/multiple-choice.d.ts +0 -30
- package/dist/tools/clarify-requirements/multiple-choice.js +0 -306
- package/dist/tools/clarify-requirements/questions-context.d.ts +0 -9
- package/dist/tools/clarify-requirements/questions-context.js +0 -74
- package/dist/tools/clarify-requirements/questions.d.ts +0 -16
- package/dist/tools/clarify-requirements/questions.js +0 -96
- package/dist/tools/clarify-requirements.d.ts +0 -4
- package/dist/tools/clarify-requirements.js +0 -314
- package/dist/tools/code-graph-handler.d.ts +0 -6
- package/dist/tools/code-graph-handler.js +0 -72
- package/dist/tools/create-rule.d.ts +0 -4
- package/dist/tools/create-rule.js +0 -107
- package/dist/tools/feedback-handler.d.ts +0 -7
- package/dist/tools/feedback-handler.js +0 -150
- package/dist/tools/semantic-search-handler.d.ts +0 -7
- package/dist/tools/semantic-search-handler.js +0 -71
- package/dist/tools/skill-registry/search.d.ts +0 -11
- package/dist/tools/skill-registry/search.js +0 -159
- package/dist/tools/tool-registry/deprecated-stubs.d.ts +0 -19
- package/dist/tools/tool-registry/deprecated-stubs.js +0 -65
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
// engine/handoff-format.ts — Handoff package formatting and enrichment summaries (SPEC-1240)
|
|
2
|
+
// Relocated from tools/package-handoff.ts so the durable executor (engine layer) can render
|
|
3
|
+
// results without importing across the tools/engine boundary.
|
|
4
|
+
import { analyzeContextPreflight, buildTokenWasteReport, loadTokenWastePolicy, recommendRelevantTools, toolsFromPolicyGroups, } from './token-optimizer/index.js';
|
|
5
|
+
import { formatProjectGraphContext, queryProjectGraphSlice } from './project-graph/index.js';
|
|
6
|
+
import { analyzeMinimalImplementation, loadMinimalImplementationPolicy, } from './minimality/index.js';
|
|
7
|
+
export function formatHandoff(pkg) {
|
|
8
|
+
const lines = [];
|
|
9
|
+
lines.push(`# Implementation Handoff Package — ${pkg.specId}`);
|
|
10
|
+
lines.push('');
|
|
11
|
+
lines.push(`Generated: ${pkg.generatedAt}`);
|
|
12
|
+
lines.push(`Readiness Score: ${pkg.readinessScore}/100`);
|
|
13
|
+
if (pkg.handoffPath) {
|
|
14
|
+
lines.push(`Handoff Path: ${pkg.handoffPath}`);
|
|
15
|
+
}
|
|
16
|
+
if (pkg.contextHash) {
|
|
17
|
+
lines.push(`Context Hash: ${pkg.contextHash}`);
|
|
18
|
+
}
|
|
19
|
+
lines.push(`Blocked: ${pkg.blocked ? 'yes' : 'no'}`);
|
|
20
|
+
lines.push('');
|
|
21
|
+
lines.push('## Objective');
|
|
22
|
+
lines.push('');
|
|
23
|
+
lines.push(pkg.objective);
|
|
24
|
+
lines.push('');
|
|
25
|
+
lines.push('## Acceptance Criteria');
|
|
26
|
+
lines.push('');
|
|
27
|
+
if (pkg.criteria.length > 0) {
|
|
28
|
+
for (const criterion of pkg.criteria) {
|
|
29
|
+
lines.push(`- [ ] ${criterion}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
lines.push('_(No criteria found — check spec.md acceptance criteria)_');
|
|
34
|
+
}
|
|
35
|
+
lines.push('');
|
|
36
|
+
lines.push('## Files to Modify');
|
|
37
|
+
lines.push('');
|
|
38
|
+
if (pkg.filesToModify.length > 0) {
|
|
39
|
+
for (const f of pkg.filesToModify) {
|
|
40
|
+
lines.push(`- ${f}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
lines.push('_(No files identified — check spec.md ## Technical / ## Files)_');
|
|
45
|
+
}
|
|
46
|
+
lines.push('');
|
|
47
|
+
lines.push('## Files to Create');
|
|
48
|
+
lines.push('');
|
|
49
|
+
if (pkg.filesToCreate.length > 0) {
|
|
50
|
+
for (const f of pkg.filesToCreate) {
|
|
51
|
+
lines.push(`- ${f}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
lines.push('_(None identified)_');
|
|
56
|
+
}
|
|
57
|
+
lines.push('');
|
|
58
|
+
lines.push('## Test Files');
|
|
59
|
+
lines.push('');
|
|
60
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- compat guard: callers may construct a HandoffPackage object predating this field
|
|
61
|
+
const filesToTest = pkg.filesToTest ?? [];
|
|
62
|
+
if (filesToTest.length > 0) {
|
|
63
|
+
for (const f of filesToTest) {
|
|
64
|
+
lines.push(`- ${f}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
lines.push('_(None identified)_');
|
|
69
|
+
}
|
|
70
|
+
lines.push('');
|
|
71
|
+
lines.push('## Ownership');
|
|
72
|
+
lines.push('');
|
|
73
|
+
if (pkg.ownership.length > 0) {
|
|
74
|
+
for (const item of pkg.ownership) {
|
|
75
|
+
lines.push(`- ${item}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
lines.push('_(Missing ownership — handoff is blocked)_');
|
|
80
|
+
}
|
|
81
|
+
lines.push('');
|
|
82
|
+
lines.push('## Test Plan');
|
|
83
|
+
lines.push('');
|
|
84
|
+
if (pkg.testPlan.length > 0) {
|
|
85
|
+
for (const item of pkg.testPlan) {
|
|
86
|
+
lines.push(`- ${item}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
lines.push('_(Missing test plan — handoff is blocked)_');
|
|
91
|
+
}
|
|
92
|
+
lines.push('');
|
|
93
|
+
lines.push('## Risks');
|
|
94
|
+
lines.push('');
|
|
95
|
+
if (pkg.risks.length > 0) {
|
|
96
|
+
for (const item of pkg.risks) {
|
|
97
|
+
lines.push(`- ${item}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
lines.push('_(Missing risk analysis — handoff is blocked)_');
|
|
102
|
+
}
|
|
103
|
+
lines.push('');
|
|
104
|
+
lines.push('## OUT OF SCOPE');
|
|
105
|
+
lines.push('');
|
|
106
|
+
lines.push('**The following must NOT be implemented:**');
|
|
107
|
+
lines.push('');
|
|
108
|
+
for (const item of pkg.outOfScope) {
|
|
109
|
+
lines.push(`- ${item}`);
|
|
110
|
+
}
|
|
111
|
+
lines.push('');
|
|
112
|
+
if (pkg.constraints.length > 0) {
|
|
113
|
+
lines.push('## Hard Constraints');
|
|
114
|
+
lines.push('');
|
|
115
|
+
for (const constraint of pkg.constraints) {
|
|
116
|
+
lines.push(`- [${constraint.type.toUpperCase()}] ${constraint.description}`);
|
|
117
|
+
if (constraint.affectedItems.length > 0) {
|
|
118
|
+
lines.push(` Affects: ${constraint.affectedItems.join(', ')}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
lines.push('');
|
|
122
|
+
}
|
|
123
|
+
if (pkg.warnings.length > 0) {
|
|
124
|
+
lines.push('## Warnings');
|
|
125
|
+
lines.push('');
|
|
126
|
+
for (const warning of pkg.warnings) {
|
|
127
|
+
lines.push(`- WARNING: ${warning}`);
|
|
128
|
+
}
|
|
129
|
+
lines.push('');
|
|
130
|
+
}
|
|
131
|
+
lines.push('## Current State');
|
|
132
|
+
lines.push('');
|
|
133
|
+
lines.push(pkg.currentState);
|
|
134
|
+
lines.push('');
|
|
135
|
+
lines.push('## Next Action');
|
|
136
|
+
lines.push('');
|
|
137
|
+
lines.push(pkg.nextAction);
|
|
138
|
+
lines.push('');
|
|
139
|
+
if (pkg.blockers.length > 0) {
|
|
140
|
+
lines.push('## Blockers');
|
|
141
|
+
lines.push('');
|
|
142
|
+
for (const blocker of pkg.blockers) {
|
|
143
|
+
lines.push(`- ${blocker}`);
|
|
144
|
+
}
|
|
145
|
+
lines.push('');
|
|
146
|
+
}
|
|
147
|
+
return lines.join('\n');
|
|
148
|
+
}
|
|
149
|
+
export function topItems(items, limit) {
|
|
150
|
+
return items.slice(0, limit);
|
|
151
|
+
}
|
|
152
|
+
export function buildHandoffSummaryText(args) {
|
|
153
|
+
const lines = [];
|
|
154
|
+
lines.push(`# Implementation Handoff Package - ${args.pkg.specId}`);
|
|
155
|
+
lines.push('');
|
|
156
|
+
lines.push(`Readiness Score: ${String(args.pkg.readinessScore)}/100`);
|
|
157
|
+
lines.push(`Blocked: ${args.pkg.blocked ? 'yes' : 'no'}`);
|
|
158
|
+
if (args.pkg.handoffPath) {
|
|
159
|
+
lines.push(`Handoff Path: ${args.pkg.handoffPath}`);
|
|
160
|
+
}
|
|
161
|
+
if (args.pkg.contextHash) {
|
|
162
|
+
lines.push(`Context Hash: ${args.pkg.contextHash}`);
|
|
163
|
+
}
|
|
164
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- compat guard: callers may construct a HandoffPackage object predating this field
|
|
165
|
+
const filesToTestCount = (args.pkg.filesToTest ?? []).length;
|
|
166
|
+
lines.push(`Counts: ${String(args.pkg.criteria.length)} criteria, ${String(args.pkg.filesToModify.length)} files to modify, ${String(args.pkg.filesToCreate.length)} files to create, ${String(filesToTestCount)} files to test, ${String(args.pkg.testPlan.length)} tests, ${String(args.pkg.risks.length)} risks.`);
|
|
167
|
+
if (args.topBlockers.length > 0) {
|
|
168
|
+
lines.push(`Top blockers: ${args.topBlockers.join('; ')}`);
|
|
169
|
+
}
|
|
170
|
+
if (args.topRisks.length > 0) {
|
|
171
|
+
lines.push(`Top risks: ${args.topRisks.join('; ')}`);
|
|
172
|
+
}
|
|
173
|
+
if (args.minimalityBlocked !== null) {
|
|
174
|
+
lines.push(`Minimality: ${args.minimalityBlocked ? 'blocked' : 'pass'}`);
|
|
175
|
+
}
|
|
176
|
+
if (args.tokenWasteRiskCount !== null) {
|
|
177
|
+
lines.push(`Token Waste Autopilot risks: ${String(args.tokenWasteRiskCount)}`);
|
|
178
|
+
}
|
|
179
|
+
lines.push(`Next Action: ${args.pkg.nextAction}`);
|
|
180
|
+
lines.push('');
|
|
181
|
+
lines.push('Full handoff is available via the handoffPath artifact; it is not embedded by default.');
|
|
182
|
+
return lines.join('\n');
|
|
183
|
+
}
|
|
184
|
+
export function summarizeMinimality(report) {
|
|
185
|
+
if (report === null) {
|
|
186
|
+
return undefined;
|
|
187
|
+
}
|
|
188
|
+
const blockingFindings = report.findings.filter((finding) => finding.blocksDone);
|
|
189
|
+
return {
|
|
190
|
+
enabled: report.enabled,
|
|
191
|
+
blocked: report.blocked,
|
|
192
|
+
findingCount: report.findings.length,
|
|
193
|
+
blockingFindingCount: blockingFindings.length,
|
|
194
|
+
blockersTop3: topItems(blockingFindings, 3).map((finding) => ({
|
|
195
|
+
ruleId: finding.ruleId,
|
|
196
|
+
target: finding.target,
|
|
197
|
+
evidence: finding.evidence,
|
|
198
|
+
})),
|
|
199
|
+
safetyExceptionCount: report.safetyExceptions.length,
|
|
200
|
+
debtEvidenceCount: report.debtEvidence.length,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
export function summarizeTokenWaste(report) {
|
|
204
|
+
if (report === null) {
|
|
205
|
+
return undefined;
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
actionsTakenCount: report.actionsTaken.length,
|
|
209
|
+
recommendationCount: report.recommendations.length,
|
|
210
|
+
riskCount: report.risks.length,
|
|
211
|
+
risksTop3: topItems(report.risks, 3).map((risk) => ({
|
|
212
|
+
decision: risk.decision,
|
|
213
|
+
target: risk.target,
|
|
214
|
+
reason: risk.reason,
|
|
215
|
+
confidence: risk.confidence,
|
|
216
|
+
})),
|
|
217
|
+
modelEffort: {
|
|
218
|
+
effort: report.modelEffort.effort,
|
|
219
|
+
reason: report.modelEffort.reason,
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
export function summarizeGraphContext(graphContext) {
|
|
224
|
+
if (graphContext === null || typeof graphContext !== 'object') {
|
|
225
|
+
return { available: false };
|
|
226
|
+
}
|
|
227
|
+
const graph = graphContext;
|
|
228
|
+
return {
|
|
229
|
+
available: true,
|
|
230
|
+
nodeCount: Array.isArray(graph.nodes) ? graph.nodes.length : 0,
|
|
231
|
+
edgeCount: Array.isArray(graph.edges) ? graph.edges.length : 0,
|
|
232
|
+
summary: graph.summary,
|
|
233
|
+
tokenSavings: graph.tokenSavings,
|
|
234
|
+
freshness: graph.freshness
|
|
235
|
+
? {
|
|
236
|
+
graphPath: graph.freshness.graphPath,
|
|
237
|
+
stale: graph.freshness.stale,
|
|
238
|
+
reason: graph.freshness.reason,
|
|
239
|
+
}
|
|
240
|
+
: undefined,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
export async function buildHandoffTokenWasteReport(pkg, knowledge, spec) {
|
|
244
|
+
const { policy } = await loadTokenWastePolicy(knowledge.projectPath);
|
|
245
|
+
if (!policy.enabled) {
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
const context = analyzeContextPreflight({
|
|
249
|
+
action: 'package_handoff',
|
|
250
|
+
policy,
|
|
251
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- compat guard: callers may construct a HandoffPackage object predating this field
|
|
252
|
+
candidates: [...pkg.filesToModify, ...pkg.filesToCreate, ...(pkg.filesToTest ?? [])].map((path) => ({
|
|
253
|
+
path,
|
|
254
|
+
reason: 'Referenced by handoff package.',
|
|
255
|
+
})),
|
|
256
|
+
spec: {
|
|
257
|
+
id: spec.id,
|
|
258
|
+
risk: spec.risk,
|
|
259
|
+
target: spec.target,
|
|
260
|
+
},
|
|
261
|
+
});
|
|
262
|
+
const tools = recommendRelevantTools({
|
|
263
|
+
action: 'package_handoff',
|
|
264
|
+
intentSignals: ['handoff', spec.scope ?? '', spec.target ?? ''].filter(Boolean),
|
|
265
|
+
tools: toolsFromPolicyGroups(policy),
|
|
266
|
+
policy,
|
|
267
|
+
});
|
|
268
|
+
return buildTokenWasteReport({
|
|
269
|
+
action: 'package_handoff',
|
|
270
|
+
policy,
|
|
271
|
+
context,
|
|
272
|
+
tools,
|
|
273
|
+
spec: {
|
|
274
|
+
id: spec.id,
|
|
275
|
+
risk: spec.risk,
|
|
276
|
+
scope: spec.scope,
|
|
277
|
+
},
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
export async function buildGraphContextSection(args) {
|
|
281
|
+
if (args.projectPath === undefined) {
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
try {
|
|
285
|
+
const slice = await queryProjectGraphSlice({
|
|
286
|
+
projectId: args.projectId,
|
|
287
|
+
projectPath: args.projectPath,
|
|
288
|
+
specId: args.specId,
|
|
289
|
+
});
|
|
290
|
+
if (slice === null || slice.nodes.length === 0) {
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
return { text: await formatProjectGraphContext(slice), structured: slice };
|
|
294
|
+
}
|
|
295
|
+
catch {
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
export async function buildMinimalImplementationReport(pkg, knowledge, spec, handoffText) {
|
|
300
|
+
const { policy, evidence } = await loadMinimalImplementationPolicy(knowledge.projectPath);
|
|
301
|
+
return analyzeMinimalImplementation({
|
|
302
|
+
policy,
|
|
303
|
+
specId: spec.id,
|
|
304
|
+
specRisk: spec.risk,
|
|
305
|
+
handoffText,
|
|
306
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- compat guard: callers may construct a HandoffPackage object predating this field
|
|
307
|
+
files: [...pkg.filesToModify, ...pkg.filesToCreate, ...(pkg.filesToTest ?? [])].map((path) => ({
|
|
308
|
+
path,
|
|
309
|
+
content: path,
|
|
310
|
+
})),
|
|
311
|
+
evidence,
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
//# sourceMappingURL=handoff-format.js.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Spec, ProjectKnowledge, HandoffPackage } from '../types/index.js';
|
|
1
|
+
import type { Spec, ProjectKnowledge, HandoffPackage, HandoffVersionFence } from '../types/index.js';
|
|
2
2
|
/** Normalize one repository-relative ownership path without retaining unsafe input in diagnostics. */
|
|
3
3
|
export declare function normalizeOwnedPath(value: string): {
|
|
4
4
|
status: 'valid';
|
|
@@ -7,7 +7,7 @@ export declare function normalizeOwnedPath(value: string): {
|
|
|
7
7
|
status: 'none';
|
|
8
8
|
} | {
|
|
9
9
|
status: 'invalid';
|
|
10
|
-
code: 'INVALID_FILE_OWNERSHIP_EMPTY' | 'INVALID_FILE_OWNERSHIP_FENCED' | 'INVALID_FILE_OWNERSHIP_NUL' | 'INVALID_FILE_OWNERSHIP_ABSOLUTE' | 'INVALID_FILE_OWNERSHIP_HOME_RELATIVE' | 'INVALID_FILE_OWNERSHIP_UNC' | 'INVALID_FILE_OWNERSHIP_DRIVE_ABSOLUTE' | 'INVALID_FILE_OWNERSHIP_URI' | 'INVALID_FILE_OWNERSHIP_TRAVERSAL' | 'INVALID_FILE_OWNERSHIP_MARKDOWN' | 'INVALID_FILE_OWNERSHIP_DESCRIPTION';
|
|
10
|
+
code: 'INVALID_FILE_OWNERSHIP_EMPTY' | 'INVALID_FILE_OWNERSHIP_FENCED' | 'INVALID_FILE_OWNERSHIP_NUL' | 'INVALID_FILE_OWNERSHIP_ABSOLUTE' | 'INVALID_FILE_OWNERSHIP_HOME_RELATIVE' | 'INVALID_FILE_OWNERSHIP_UNC' | 'INVALID_FILE_OWNERSHIP_DRIVE_ABSOLUTE' | 'INVALID_FILE_OWNERSHIP_URI' | 'INVALID_FILE_OWNERSHIP_TRAVERSAL' | 'INVALID_FILE_OWNERSHIP_MARKDOWN' | 'INVALID_FILE_OWNERSHIP_DESCRIPTION' | 'INVALID_FILE_OWNERSHIP_CHARSET';
|
|
11
11
|
};
|
|
12
12
|
/** Normalize a raw traceability changed-file value without interpreting Markdown or prose. */
|
|
13
13
|
export declare function normalizeChangedFilePath(value: string): ReturnType<typeof normalizeOwnedPath>;
|
|
@@ -21,5 +21,7 @@ export declare function extractCanonicalFileOwnership(content: string): {
|
|
|
21
21
|
toTestDeclared: string[];
|
|
22
22
|
blockers: string[];
|
|
23
23
|
};
|
|
24
|
-
export declare function packageHandoff(spec: Spec, knowledge: ProjectKnowledge): Promise<HandoffPackage
|
|
24
|
+
export declare function packageHandoff(spec: Spec, knowledge: ProjectKnowledge, versionFence?: HandoffVersionFence): Promise<HandoffPackage & {
|
|
25
|
+
readonly staleVersionSkipped?: boolean;
|
|
26
|
+
}>;
|
|
25
27
|
//# sourceMappingURL=handoff-packager.d.ts.map
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// engine/handoff-packager.ts — Assembles handoff context package for AI agents (SPEC-039)
|
|
2
|
-
|
|
3
|
-
import {
|
|
2
|
+
/* eslint-disable max-lines -- package assembly and the SPEC-1240 atomic-publish/version-fence path stay co-located for auditability */
|
|
3
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
4
|
+
import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
4
5
|
import { join } from 'node:path';
|
|
5
6
|
import { stripFrontmatter } from './frontmatter-parser.js';
|
|
6
7
|
// eslint-disable-next-line no-restricted-imports -- grandfathered layer violation, remediation SPEC-1661 SPEC-1662 SPEC-1663
|
|
@@ -118,6 +119,7 @@ function ownershipItems(content) {
|
|
|
118
119
|
}
|
|
119
120
|
return { items: extractListItems(visibleLines.join('\n')), fenced };
|
|
120
121
|
}
|
|
122
|
+
const OWNERSHIP_DESCRIPTION_SEPARATOR = /\s+(?:—|–|-)\s+/u;
|
|
121
123
|
/** Normalize one repository-relative ownership path without retaining unsafe input in diagnostics. */
|
|
122
124
|
export function normalizeOwnedPath(value) {
|
|
123
125
|
const trimmed = value.trim();
|
|
@@ -125,7 +127,9 @@ export function normalizeOwnedPath(value) {
|
|
|
125
127
|
return { status: 'none' };
|
|
126
128
|
}
|
|
127
129
|
const backticked = /^`([^`]+)`/.exec(trimmed)?.[1];
|
|
128
|
-
const candidate = (backticked ??
|
|
130
|
+
const candidate = (backticked ??
|
|
131
|
+
trimmed.split(new RegExp(`${OWNERSHIP_DESCRIPTION_SEPARATOR.source}|\\s+\\(`, 'u'), 1)[0] ??
|
|
132
|
+
'').trim();
|
|
129
133
|
if (candidate.length === 0) {
|
|
130
134
|
return { status: 'invalid', code: 'INVALID_FILE_OWNERSHIP_EMPTY' };
|
|
131
135
|
}
|
|
@@ -323,34 +327,63 @@ function stripInertOwnershipMarkup(content) {
|
|
|
323
327
|
function normalizeStrictOwnedPath(rawValue) {
|
|
324
328
|
const normalized = normalizeOwnedPath(rawValue);
|
|
325
329
|
if (normalized.status !== 'valid') {
|
|
326
|
-
return
|
|
330
|
+
return normalized;
|
|
327
331
|
}
|
|
328
|
-
return /^[A-Za-z0-9._/()[\]@-]+$/u.test(normalized.path)
|
|
332
|
+
return /^[A-Za-z0-9._/()[\]@-]+$/u.test(normalized.path)
|
|
333
|
+
? normalized
|
|
334
|
+
: { status: 'invalid', code: 'INVALID_FILE_OWNERSHIP_CHARSET' };
|
|
335
|
+
}
|
|
336
|
+
function ownershipHeadEnd(rawPath) {
|
|
337
|
+
const masked = rawPath.replaceAll(/`[^`\n]+`/gu, (match) => 'x'.repeat(match.length));
|
|
338
|
+
return OWNERSHIP_DESCRIPTION_SEPARATOR.exec(masked)?.index ?? rawPath.length;
|
|
339
|
+
}
|
|
340
|
+
const ownershipReasonCode = (result) => result.status === 'invalid' ? result.code : 'INVALID_FILE_OWNERSHIP_EMPTY';
|
|
341
|
+
function ownershipInvalidTokenBlocker(token, classification, reasonCode) {
|
|
342
|
+
const printed = JSON.stringify(token.length > 120 ? token.slice(0, 120) : token);
|
|
343
|
+
const hint = `${OWNERSHIP_LINE_SHAPE_HINT} reason=${reasonCode}`;
|
|
344
|
+
return ownershipBlocker(printed, [classification, 'InvalidPath'], hint);
|
|
329
345
|
}
|
|
330
346
|
function collectStrictOwnershipEntry(rawValue, classification, state) {
|
|
331
347
|
const rawPath = rawValue.trim();
|
|
332
|
-
const
|
|
333
|
-
const
|
|
334
|
-
const
|
|
335
|
-
const
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
348
|
+
const head = rawPath.slice(0, ownershipHeadEnd(rawPath));
|
|
349
|
+
const backtickMatches = [...head.matchAll(/`([^`\n]+)`/gu)].map((match) => match[1] ?? '');
|
|
350
|
+
const pathCandidates = backtickMatches.filter((token) => /[./]/u.test(token));
|
|
351
|
+
const pushInvalid = (token, code) => {
|
|
352
|
+
state.blockers.push(ownershipInvalidTokenBlocker(token, classification, code));
|
|
353
|
+
};
|
|
354
|
+
if (!/`[^`\n]+`/u.test(rawPath)) {
|
|
355
|
+
const direct = normalizeStrictOwnedPath(head);
|
|
356
|
+
if (direct.status !== 'valid') {
|
|
357
|
+
pushInvalid(head, ownershipReasonCode(direct));
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
collectStrictOwnershipPath(direct.path, classification, state);
|
|
339
361
|
return;
|
|
340
362
|
}
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
363
|
+
if (backtickMatches.length === 0) {
|
|
364
|
+
pushInvalid(head, 'INVALID_FILE_OWNERSHIP_HEAD_RESIDUAL');
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
const residual = head.replaceAll(/`[^`\n]+`/gu, '').trim();
|
|
368
|
+
if (!isSafeOwnershipResidual(residual)) {
|
|
369
|
+
pushInvalid(residual, 'INVALID_FILE_OWNERSHIP_HEAD_RESIDUAL');
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
const normalizedCandidates = pathCandidates.map((token) => ({
|
|
373
|
+
token,
|
|
374
|
+
result: normalizeStrictOwnedPath(token),
|
|
375
|
+
}));
|
|
376
|
+
const failed = normalizedCandidates.filter(({ result }) => result.status !== 'valid');
|
|
377
|
+
if (failed.length > 0) {
|
|
378
|
+
for (const { token, result } of failed) {
|
|
379
|
+
pushInvalid(token, ownershipReasonCode(result));
|
|
380
|
+
}
|
|
350
381
|
return;
|
|
351
382
|
}
|
|
352
|
-
for (const
|
|
353
|
-
|
|
383
|
+
for (const { result } of normalizedCandidates) {
|
|
384
|
+
if (result.status === 'valid') {
|
|
385
|
+
collectStrictOwnershipPath(result.path, classification, state);
|
|
386
|
+
}
|
|
354
387
|
}
|
|
355
388
|
}
|
|
356
389
|
function collectStrictOwnershipPath(path, classification, state) {
|
|
@@ -435,7 +468,7 @@ function extractFileLevelWorkPlanOwnership(content) {
|
|
|
435
468
|
};
|
|
436
469
|
}
|
|
437
470
|
function validateOwnershipAgreement(canonical, workPlan, testDeclaredPaths) {
|
|
438
|
-
if (!canonical.present
|
|
471
|
+
if (!canonical.present) {
|
|
439
472
|
return [];
|
|
440
473
|
}
|
|
441
474
|
const testDeclared = new Set(testDeclaredPaths);
|
|
@@ -444,6 +477,13 @@ function validateOwnershipAgreement(canonical, workPlan, testDeclaredPaths) {
|
|
|
444
477
|
: ownership.toModify.includes(path)
|
|
445
478
|
? 'Modify'
|
|
446
479
|
: 'Absent';
|
|
480
|
+
if (!workPlan.present) {
|
|
481
|
+
const canonicalPaths = [...canonical.toCreate, ...canonical.toModify].filter((path) => !testDeclared.has(path));
|
|
482
|
+
return [...new Set(canonicalPaths)].sort().map((path) => {
|
|
483
|
+
const canonicalClass = classification(canonical, path);
|
|
484
|
+
return ownershipBlocker(path, [`Canonical:${canonicalClass}`, 'WorkPlan:Absent'], ownershipAgreementHint(canonicalClass, 'Absent', path));
|
|
485
|
+
});
|
|
486
|
+
}
|
|
447
487
|
const paths = new Set([
|
|
448
488
|
...canonical.toCreate,
|
|
449
489
|
...canonical.toModify,
|
|
@@ -463,9 +503,9 @@ function validateOwnershipAgreement(canonical, workPlan, testDeclaredPaths) {
|
|
|
463
503
|
function extractBacktickedFiles(content) {
|
|
464
504
|
const files = new Set();
|
|
465
505
|
for (const match of content.matchAll(/`([^`\n]+)`/gu)) {
|
|
466
|
-
const
|
|
467
|
-
if (
|
|
468
|
-
files.add(path);
|
|
506
|
+
const normalized = match[1] ? normalizeStrictOwnedPath(match[1]) : null;
|
|
507
|
+
if (normalized?.status === 'valid') {
|
|
508
|
+
files.add(normalized.path);
|
|
469
509
|
}
|
|
470
510
|
}
|
|
471
511
|
return [...files].sort();
|
|
@@ -602,7 +642,7 @@ function buildConstraints(knowledge) {
|
|
|
602
642
|
return constraints;
|
|
603
643
|
}
|
|
604
644
|
// ── Public API ───────────────────────────────────────────────────────────────
|
|
605
|
-
export async function packageHandoff(spec, knowledge) {
|
|
645
|
+
export async function packageHandoff(spec, knowledge, versionFence) {
|
|
606
646
|
const warnings = [];
|
|
607
647
|
const rawSpecContent = spec.specPath
|
|
608
648
|
? await readFile(spec.specPath, 'utf-8').catch(() => '')
|
|
@@ -685,7 +725,7 @@ export async function packageHandoff(spec, knowledge) {
|
|
|
685
725
|
blocked: blockers.length > 0,
|
|
686
726
|
blockers,
|
|
687
727
|
};
|
|
688
|
-
return persistHandoffIfPossible(pkg, knowledge);
|
|
728
|
+
return persistHandoffIfPossible(pkg, knowledge, versionFence);
|
|
689
729
|
}
|
|
690
730
|
function renderPersistedHandoff(pkg) {
|
|
691
731
|
return [
|
|
@@ -733,19 +773,36 @@ function renderPersistedHandoff(pkg) {
|
|
|
733
773
|
'',
|
|
734
774
|
].join('\n');
|
|
735
775
|
}
|
|
736
|
-
async function persistHandoffIfPossible(pkg, knowledge) {
|
|
776
|
+
async function persistHandoffIfPossible(pkg, knowledge, versionFence) {
|
|
737
777
|
if (!knowledge.projectPath) {
|
|
738
778
|
return pkg;
|
|
739
779
|
}
|
|
740
780
|
const handoffDir = join(projectDataDir(hashProjectPath(knowledge.projectPath)), 'handoffs');
|
|
741
781
|
const handoffPath = join(handoffDir, `${pkg.specId}.md`);
|
|
782
|
+
const tempPath = join(handoffDir, `.${pkg.specId}.md.tmp-${randomUUID()}`);
|
|
742
783
|
const sessionContextPath = join(knowledge.projectPath, 'planu', 'session-context.md');
|
|
743
784
|
const markdown = renderPersistedHandoff(pkg);
|
|
744
785
|
try {
|
|
745
786
|
await mkdir(handoffDir, { recursive: true });
|
|
746
|
-
await writeFile(
|
|
787
|
+
await writeFile(tempPath, markdown, 'utf-8');
|
|
788
|
+
if (versionFence) {
|
|
789
|
+
const abandoned = versionFence.signal?.aborted === true;
|
|
790
|
+
const currentSpecVersion = abandoned ? undefined : await versionFence.getCurrentSpecVersion();
|
|
791
|
+
if (abandoned ||
|
|
792
|
+
(currentSpecVersion !== undefined && currentSpecVersion !== versionFence.specVersion)) {
|
|
793
|
+
await rm(tempPath, { force: true });
|
|
794
|
+
return { ...pkg, staleVersionSkipped: true };
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
await rename(tempPath, handoffPath);
|
|
747
798
|
}
|
|
748
|
-
catch {
|
|
799
|
+
catch (error) {
|
|
800
|
+
await rm(tempPath, { force: true }).catch(() => {
|
|
801
|
+
/* best-effort temp cleanup — the publish already failed */
|
|
802
|
+
});
|
|
803
|
+
if (versionFence) {
|
|
804
|
+
throw error;
|
|
805
|
+
}
|
|
749
806
|
return {
|
|
750
807
|
...pkg,
|
|
751
808
|
warnings: [
|
|
@@ -7,63 +7,34 @@ import { fileURLToPath } from 'node:url';
|
|
|
7
7
|
// Core AGENTS.md tool reference — curated list of key Planu tools
|
|
8
8
|
// ---------------------------------------------------------------------------
|
|
9
9
|
export const CORE_AGENTS_MD_TOOLS = [
|
|
10
|
+
{ name: 'init_project', description: 'Load or bootstrap Planu project context' },
|
|
11
|
+
{ name: 'facilitate', description: 'Universal entry point for SDD workflow' },
|
|
10
12
|
{ name: 'create_spec', description: 'Create a new spec from a description' },
|
|
11
13
|
{ name: 'list_specs', description: 'List all specs and their status' },
|
|
12
14
|
{ name: 'update_status', description: 'Move a spec through workflow stages' },
|
|
15
|
+
{
|
|
16
|
+
name: 'update_status_batch',
|
|
17
|
+
description: 'Batch update many specs to the same status in one MCP execution',
|
|
18
|
+
},
|
|
13
19
|
{ name: 'validate', description: 'Check implementation against spec criteria' },
|
|
14
20
|
{ name: 'challenge_spec', description: 'Stress-test a spec for gaps before implementation' },
|
|
15
21
|
{ name: 'check_readiness', description: 'Verify spec has enough detail to implement' },
|
|
16
|
-
{ name: 'planu_status', description: 'Quick project health snapshot' },
|
|
17
|
-
{ name: 'facilitate', description: 'Universal entry point for SDD workflow' },
|
|
18
|
-
{ name: 'plan_mode', description: 'Preview implementation plan before writing code' },
|
|
19
|
-
{ name: 'generate_steering_file', description: 'Generate host-aware steering files' },
|
|
20
|
-
{ name: 'search_specs', description: 'Search specs with fuzzy text matching' },
|
|
21
|
-
{ name: 'spec_quality_score', description: 'Compute quality score for a spec' },
|
|
22
|
-
{ name: 'analyze_spec_size', description: 'Analyze if a spec should be split' },
|
|
23
|
-
{ name: 'decompose_spec', description: 'Decompose a spec into agent tasks' },
|
|
24
|
-
{ name: 'estimate', description: 'Estimate time and cost for a spec' },
|
|
25
|
-
{ name: 'session_checkpoint', description: 'Save session context' },
|
|
26
|
-
{ name: 'project_overview', description: 'Single-call project state query' },
|
|
27
|
-
{ name: 'scan_project', description: 'Scan project and generate specs' },
|
|
28
|
-
{ name: 'validate_workflow', description: 'Validate a workflow definition' },
|
|
29
|
-
{ name: 'tdd_scaffold', description: 'Generate TDD test stubs from spec criteria' },
|
|
30
|
-
{ name: 'coverage_gap_analyzer', description: 'Analyze untested acceptance criteria' },
|
|
31
|
-
{ name: 'security_scan', description: 'Scan dependencies for vulnerabilities' },
|
|
32
|
-
{ name: 'bootstrap_skills', description: 'Auto-install skills for detected stack' },
|
|
33
|
-
{ name: 'manage_drift', description: 'Detect and resolve spec drift' },
|
|
34
|
-
{ name: 'sync_spec_state', description: 'Sync spec status between frontmatter and store' },
|
|
35
|
-
{ name: 'health_check_all', description: 'Run health check on all specs' },
|
|
36
|
-
{ name: 'tokens', description: 'Token usage dashboard' },
|
|
37
|
-
{ name: 'configure_workers', description: 'Configure background analysis workers' },
|
|
38
22
|
{
|
|
39
|
-
name: '
|
|
40
|
-
description:
|
|
23
|
+
name: 'bump_spec_version',
|
|
24
|
+
description: "Bump the SemVer spec_version of a spec's frontmatter",
|
|
25
|
+
},
|
|
26
|
+
{ name: 'reconcile_spec', description: 'Reconcile implementation drift back into the spec' },
|
|
27
|
+
{
|
|
28
|
+
name: 'migrate_legacy_spec',
|
|
29
|
+
description: 'Persist a synthesized spec.md and retire legacy files',
|
|
41
30
|
},
|
|
42
|
-
{ name: '
|
|
43
|
-
{ name: '
|
|
44
|
-
{ name: '
|
|
45
|
-
{ name: '
|
|
46
|
-
{ name: '
|
|
47
|
-
{ name: '
|
|
48
|
-
{ name: '
|
|
49
|
-
{ name: 'render_spec_for_provider', description: 'Render spec for target LLM provider' },
|
|
50
|
-
{ name: 'convert_to_bdd', description: 'Convert spec criteria to BDD format' },
|
|
51
|
-
{ name: 'generate_e2e_tests', description: 'Generate E2E tests from spec criteria' },
|
|
52
|
-
{ name: 'run_spec_lint', description: 'Run custom lint rules against specs' },
|
|
53
|
-
{ name: 'suggest_criteria', description: 'Suggest missing best-practice criteria' },
|
|
54
|
-
{ name: 'check_compliance', description: 'Unified compliance tool' },
|
|
55
|
-
{ name: 'create_release', description: 'Create a GitHub Release' },
|
|
56
|
-
{ name: 'generate_changelog', description: 'Generate changelog from merged PRs' },
|
|
57
|
-
{ name: 'create_pr_from_spec', description: 'Create a PR from a spec' },
|
|
58
|
-
{ name: 'onboarding_progress', description: 'View onboarding progress' },
|
|
59
|
-
{ name: 'list_templates', description: 'Browse spec template catalog' },
|
|
60
|
-
{ name: 'generate_docs_site', description: 'Generate static HTML docs site' },
|
|
61
|
-
{ name: 'plan_team_distribution', description: 'Distribute specs across agents' },
|
|
62
|
-
{ name: 'optimize_spec_scheduling', description: 'Minimize agent idle time' },
|
|
63
|
-
{ name: 'context_window_status', description: 'Estimate token consumption' },
|
|
64
|
-
{ name: 'archive_spec_context', description: 'Archive a spec to free tokens' },
|
|
65
|
-
{ name: 'workspace_overview', description: 'Get overview of all projects' },
|
|
66
|
-
{ name: 'search_all_projects', description: 'Search specs across all projects' },
|
|
31
|
+
{ name: 'package_handoff', description: 'Generate an implementation handoff package for a spec' },
|
|
32
|
+
{ name: 'planu_status', description: 'Quick project health snapshot' },
|
|
33
|
+
{ name: 'session_checkpoint', description: 'Save session context' },
|
|
34
|
+
{ name: 'estimate', description: 'Estimate time and cost for a spec' },
|
|
35
|
+
{ name: 'request_changes', description: 'Request changes on a spec, moving it back to draft' },
|
|
36
|
+
{ name: 'get_job', description: 'Read the durable state of a local Planu job' },
|
|
37
|
+
{ name: 'restart_job', description: 'Restart a cancelled or failed durable job' },
|
|
67
38
|
];
|
|
68
39
|
// ---------------------------------------------------------------------------
|
|
69
40
|
// Interactive detection heuristics
|
|
@@ -89,20 +89,31 @@ export function buildListSpecsSummary(total, byStatus) {
|
|
|
89
89
|
return `No plans found in this project yet. Use create_spec to add your first one.`;
|
|
90
90
|
}
|
|
91
91
|
const get = (key) => byStatus[key] ?? 0;
|
|
92
|
-
const inProgress = get('implementing')
|
|
92
|
+
const inProgress = get('implementing');
|
|
93
|
+
const approved = get('approved');
|
|
93
94
|
const inReview = get('review');
|
|
95
|
+
const approvedClause = approved > 0 ? `${String(approved)} approved ready to implement` : '';
|
|
94
96
|
if (inReview > 0 && inProgress > 0) {
|
|
95
97
|
const reviewNoun = inReview === 1 ? 'plan' : 'plans';
|
|
96
|
-
|
|
98
|
+
const suffix = approvedClause ? ` and ${approvedClause}` : '';
|
|
99
|
+
return `You have ${String(inProgress)} in progress and ${String(inReview)} ${reviewNoun} ready to review${suffix}. Start with the ones under review.`;
|
|
97
100
|
}
|
|
98
101
|
if (inReview > 0) {
|
|
99
102
|
const noun = inReview === 1 ? 'plan' : 'plans';
|
|
100
|
-
|
|
103
|
+
const suffix = approvedClause ? ` You also have ${approvedClause}.` : '';
|
|
104
|
+
return `You have ${String(inReview)} ${noun} ready to review. Approve or request changes to move forward.${suffix}`;
|
|
105
|
+
}
|
|
106
|
+
if (inProgress > 0 && approved > 0) {
|
|
107
|
+
return `You have ${String(inProgress)} in progress and ${approvedClause}. Run validate when implementation is done.`;
|
|
101
108
|
}
|
|
102
109
|
if (inProgress > 0) {
|
|
103
110
|
const noun = inProgress === 1 ? 'plan' : 'plans';
|
|
104
111
|
return `You have ${String(inProgress)} ${noun} in progress. Run validate when implementation is done.`;
|
|
105
112
|
}
|
|
113
|
+
if (approved > 0) {
|
|
114
|
+
const noun = approved === 1 ? 'plan' : 'plans';
|
|
115
|
+
return `You have ${String(approved)} ${noun} approved ready to implement. Run validate when implementation is done.`;
|
|
116
|
+
}
|
|
106
117
|
return `You have ${String(total)} plans in this project. Approve a plan to start implementing.`;
|
|
107
118
|
}
|
|
108
119
|
/** estimate — time and cost estimated for a spec */
|