@planu/cli 5.7.5 → 5.7.6
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 +75 -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-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 +1 -1
- 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/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 +27 -0
- 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 +30 -10
- 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.js +115 -1
- 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/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/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/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-sdd-tools.js +0 -23
- package/dist/tools/register-spec-tools/core-spec-tools.js +18 -27
- 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 +1 -140
- package/dist/tools/tool-registry/group-infra.js +2 -39
- package/dist/tools/tool-registry/group-quality-compliance.js +9 -1
- 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 +12 -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
|
@@ -1,15 +1,12 @@
|
|
|
1
|
-
// engine/session/session-tracker.ts — In-memory call counter and file modification tracker
|
|
2
|
-
// for auto session checkpointing (SPEC-563).
|
|
3
1
|
const DEFAULT_THRESHOLD = 10;
|
|
4
2
|
const MAX_LAST_CALLS = 10;
|
|
5
3
|
const MAX_ERRORS = 20;
|
|
6
|
-
|
|
4
|
+
let sharedPolicy = { threshold: DEFAULT_THRESHOLD, enabled: true };
|
|
5
|
+
export class SessionTracker {
|
|
7
6
|
callCount = 0;
|
|
8
7
|
lastCalls = [];
|
|
9
8
|
modifiedFiles = new Set();
|
|
10
9
|
errors = [];
|
|
11
|
-
policy = { threshold: DEFAULT_THRESHOLD, enabled: true };
|
|
12
|
-
/** Record a tool call. Appends to lastCalls (capped at MAX_LAST_CALLS). */
|
|
13
10
|
recordCall(tool, success) {
|
|
14
11
|
this.callCount++;
|
|
15
12
|
this.lastCalls.push({ tool, success, timestamp: new Date().toISOString() });
|
|
@@ -17,25 +14,18 @@ class SessionTracker {
|
|
|
17
14
|
this.lastCalls.shift();
|
|
18
15
|
}
|
|
19
16
|
}
|
|
20
|
-
/** Record a file path as modified during this session. */
|
|
21
17
|
recordFile(path) {
|
|
22
18
|
this.modifiedFiles.add(path);
|
|
23
19
|
}
|
|
24
|
-
/** Record an error message (capped at MAX_ERRORS). */
|
|
25
20
|
recordError(msg) {
|
|
26
21
|
if (this.errors.length >= MAX_ERRORS) {
|
|
27
22
|
this.errors.shift();
|
|
28
23
|
}
|
|
29
24
|
this.errors.push(msg);
|
|
30
25
|
}
|
|
31
|
-
/**
|
|
32
|
-
* Returns true when a checkpoint should be written:
|
|
33
|
-
* policy is enabled, callCount > 0, and callCount is a multiple of threshold.
|
|
34
|
-
*/
|
|
35
26
|
shouldCheckpoint() {
|
|
36
|
-
return (
|
|
27
|
+
return (sharedPolicy.enabled && this.callCount > 0 && this.callCount % sharedPolicy.threshold === 0);
|
|
37
28
|
}
|
|
38
|
-
/** Return a compact snapshot of the current session state. */
|
|
39
29
|
getSnapshot() {
|
|
40
30
|
return {
|
|
41
31
|
callCount: this.callCount,
|
|
@@ -44,28 +34,41 @@ class SessionTracker {
|
|
|
44
34
|
errors: [...this.errors],
|
|
45
35
|
};
|
|
46
36
|
}
|
|
47
|
-
/** Update the checkpoint policy (partial update — unspecified fields keep current values). */
|
|
48
37
|
setPolicy(p) {
|
|
49
|
-
|
|
50
|
-
this.policy = { ...this.policy, threshold: Math.max(1, Math.round(p.threshold)) };
|
|
51
|
-
}
|
|
52
|
-
if (p.enabled !== undefined) {
|
|
53
|
-
this.policy = { ...this.policy, enabled: p.enabled };
|
|
54
|
-
}
|
|
38
|
+
setCheckpointPolicy(p);
|
|
55
39
|
}
|
|
56
|
-
/** Return the current checkpoint policy. */
|
|
57
40
|
getPolicy() {
|
|
58
|
-
return
|
|
41
|
+
return getCheckpointPolicy();
|
|
59
42
|
}
|
|
60
|
-
/** Reset the tracker state (useful for testing). */
|
|
61
43
|
reset() {
|
|
62
44
|
this.callCount = 0;
|
|
63
45
|
this.lastCalls.splice(0);
|
|
64
46
|
this.modifiedFiles.clear();
|
|
65
47
|
this.errors.splice(0);
|
|
66
|
-
this.policy = { threshold: DEFAULT_THRESHOLD, enabled: true };
|
|
67
48
|
}
|
|
68
49
|
}
|
|
69
|
-
|
|
70
|
-
export
|
|
50
|
+
const trackers = new Map();
|
|
51
|
+
export function getSessionTracker(projectPath) {
|
|
52
|
+
let tracker = trackers.get(projectPath);
|
|
53
|
+
if (!tracker) {
|
|
54
|
+
tracker = new SessionTracker();
|
|
55
|
+
trackers.set(projectPath, tracker);
|
|
56
|
+
}
|
|
57
|
+
return tracker;
|
|
58
|
+
}
|
|
59
|
+
export function setCheckpointPolicy(p) {
|
|
60
|
+
if (p.threshold !== undefined) {
|
|
61
|
+
sharedPolicy = { ...sharedPolicy, threshold: Math.max(1, Math.round(p.threshold)) };
|
|
62
|
+
}
|
|
63
|
+
if (p.enabled !== undefined) {
|
|
64
|
+
sharedPolicy = { ...sharedPolicy, enabled: p.enabled };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
export function getCheckpointPolicy() {
|
|
68
|
+
return { ...sharedPolicy };
|
|
69
|
+
}
|
|
70
|
+
export function resetSessionTrackers() {
|
|
71
|
+
trackers.clear();
|
|
72
|
+
sharedPolicy = { threshold: DEFAULT_THRESHOLD, enabled: true };
|
|
73
|
+
}
|
|
71
74
|
//# sourceMappingURL=session-tracker.js.map
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
// engine/session-state/writer.ts — SPEC-633: Write session.json universal state file
|
|
3
3
|
import { readFile, mkdir } from 'node:fs/promises';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
|
-
import { execSync } from 'node:child_process';
|
|
6
5
|
import { stableJsonStringify, stableWriteGeneratedFile, stripRootUpdatedAt, } from '../generated-artifact-writer.js';
|
|
6
|
+
import { execGitCapture } from '../git/exec-git.js';
|
|
7
7
|
// eslint-disable-next-line no-restricted-imports -- grandfathered layer violation, remediation SPEC-1699
|
|
8
8
|
import { projectRuntimeDir } from '../../storage/storage-layout.js';
|
|
9
9
|
const ROOT_SESSION = 'session.json';
|
|
@@ -18,11 +18,7 @@ export function rootFile(projectPath) {
|
|
|
18
18
|
}
|
|
19
19
|
function getModifiedFiles(projectPath) {
|
|
20
20
|
try {
|
|
21
|
-
const out =
|
|
22
|
-
cwd: projectPath,
|
|
23
|
-
encoding: 'utf-8',
|
|
24
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
25
|
-
});
|
|
21
|
+
const out = execGitCapture('git diff --name-only HEAD 2>/dev/null', projectPath);
|
|
26
22
|
return out.trim().split('\n').filter(Boolean).slice(0, 10);
|
|
27
23
|
}
|
|
28
24
|
catch {
|
|
@@ -31,11 +27,7 @@ function getModifiedFiles(projectPath) {
|
|
|
31
27
|
}
|
|
32
28
|
function getBranch(projectPath) {
|
|
33
29
|
try {
|
|
34
|
-
const out =
|
|
35
|
-
cwd: projectPath,
|
|
36
|
-
encoding: 'utf-8',
|
|
37
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
38
|
-
});
|
|
30
|
+
const out = execGitCapture('git rev-parse --abbrev-ref HEAD 2>/dev/null', projectPath);
|
|
39
31
|
return out.trim() || null;
|
|
40
32
|
}
|
|
41
33
|
catch {
|
|
@@ -3,8 +3,10 @@ import type { WorkflowSkillGenerationOptions, WorkflowSkillGenerationResult } fr
|
|
|
3
3
|
export type { WorkflowSkillGenerationOptions, WorkflowSkillGenerationResult, } from '../../types/skill-registry.js';
|
|
4
4
|
/**
|
|
5
5
|
* Generate all 22 workflow skill files for the given project.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* Refreshes a pristine copy when Planu's shipped body changes; never touches
|
|
7
|
+
* a user-authored or user-edited file (SPEC-1457).
|
|
8
|
+
* Writes registry entries via writeSkillRegistryEntry (SPEC-680) only when a
|
|
9
|
+
* file is actually rewritten.
|
|
8
10
|
*/
|
|
9
11
|
export declare function generateWorkflowSkills(projectPath: string, knowledge: ProjectKnowledge, options?: WorkflowSkillGenerationOptions): Promise<WorkflowSkillGenerationResult>;
|
|
10
12
|
//# sourceMappingURL=workflow-skill-generator.d.ts.map
|
|
@@ -1,28 +1,35 @@
|
|
|
1
1
|
import { technologyValue } from '../technology-registry.js';
|
|
2
|
-
// src/engine/skill-generator/workflow-skill-generator.ts — SPEC-685
|
|
2
|
+
// src/engine/skill-generator/workflow-skill-generator.ts — SPEC-685, refreshed by SPEC-1457
|
|
3
3
|
// Generates all 22 workflow skill files for a project.
|
|
4
4
|
// Delegates placeholder substitution to adaptSkillContent (SPEC-682 — DRY).
|
|
5
5
|
// Registers each skill via writeSkillRegistryEntry (SPEC-680).
|
|
6
6
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
7
|
+
import { createHash } from 'node:crypto';
|
|
7
8
|
import { join } from 'node:path';
|
|
8
9
|
import { adaptSkillContent } from '../skill-adapter/index.js';
|
|
9
10
|
// eslint-disable-next-line no-restricted-imports -- grandfathered layer violation, remediation SPEC-1661 SPEC-1662 SPEC-1663
|
|
10
11
|
import { writeSkillRegistryEntry } from '../../storage/skill-registry-storage.js';
|
|
11
12
|
import { WORKFLOW_SKILL_DEFINITIONS } from './skill-definitions.js';
|
|
12
|
-
import { hashConventions } from './conventions-hasher.js';
|
|
13
13
|
// ---------------------------------------------------------------------------
|
|
14
14
|
// Constants
|
|
15
15
|
// ---------------------------------------------------------------------------
|
|
16
16
|
const CLAUDE_SKILLS_REL = '.claude/skills';
|
|
17
|
-
const HASH_MARKER_PREFIX = '<!-- planu-
|
|
17
|
+
const HASH_MARKER_PREFIX = '<!-- planu-skill-hash: ';
|
|
18
18
|
const HASH_MARKER_SUFFIX = ' -->';
|
|
19
|
+
const LEGACY_HASH_MARKER_PREFIX = '<!-- planu-conventions-hash: ';
|
|
20
|
+
const MARKER_SCAN_WINDOW = 5;
|
|
21
|
+
const HEX_HASH_PATTERN = /^[0-9a-f]{64}$/;
|
|
22
|
+
const LEGACY_HEX_HASH_PATTERN = /^[0-9a-f]{16}$/;
|
|
19
23
|
// ---------------------------------------------------------------------------
|
|
20
24
|
// Helpers
|
|
21
25
|
// ---------------------------------------------------------------------------
|
|
22
|
-
/**
|
|
26
|
+
/** SHA-256 hex digest of the exact bytes of the rendered skill content. */
|
|
27
|
+
function digestContent(content) {
|
|
28
|
+
return createHash('sha256').update(content, 'utf8').digest('hex');
|
|
29
|
+
}
|
|
30
|
+
/** Build the full skill markdown with the content-bound hash marker on line 2. */
|
|
23
31
|
function buildSkillContent(adaptedBody, hash) {
|
|
24
32
|
const marker = `${HASH_MARKER_PREFIX}${hash}${HASH_MARKER_SUFFIX}`;
|
|
25
|
-
// Insert hash marker as second line (after the first line of the body)
|
|
26
33
|
const lines = adaptedBody.split('\n');
|
|
27
34
|
if (lines.length < 2) {
|
|
28
35
|
return adaptedBody + '\n' + marker + '\n';
|
|
@@ -30,16 +37,58 @@ function buildSkillContent(adaptedBody, hash) {
|
|
|
30
37
|
lines.splice(1, 0, marker);
|
|
31
38
|
return lines.join('\n');
|
|
32
39
|
}
|
|
33
|
-
/**
|
|
34
|
-
function
|
|
40
|
+
/** Rendered content with the marker line at `lineIndex` removed, for the pristine digest check. */
|
|
41
|
+
function contentWithoutMarkerLine(content, lineIndex) {
|
|
42
|
+
const lines = content.split('\n');
|
|
43
|
+
lines.splice(lineIndex, 1);
|
|
44
|
+
return lines.join('\n');
|
|
45
|
+
}
|
|
46
|
+
function extractMarkerValue(line, prefix) {
|
|
47
|
+
if (!line.startsWith(prefix) || !line.endsWith(HASH_MARKER_SUFFIX)) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
return line.slice(prefix.length, -HASH_MARKER_SUFFIX.length);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Fail-closed marker reader: any duplicate marker, mixed marker formats, or a
|
|
54
|
+
* malformed hash value is reported as 'invalid' so the caller treats the file
|
|
55
|
+
* as user-modified rather than risk overwriting an edit.
|
|
56
|
+
*/
|
|
57
|
+
function readMarker(content) {
|
|
35
58
|
const lines = content.split('\n');
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
59
|
+
const window = lines.slice(0, MARKER_SCAN_WINDOW);
|
|
60
|
+
let currentHash = '';
|
|
61
|
+
let currentLineIndex = -1;
|
|
62
|
+
let currentCount = 0;
|
|
63
|
+
let legacyHash = '';
|
|
64
|
+
let legacyCount = 0;
|
|
65
|
+
for (let lineIndex = 0; lineIndex < window.length; lineIndex++) {
|
|
66
|
+
const line = window[lineIndex] ?? '';
|
|
67
|
+
const currentValue = extractMarkerValue(line, HASH_MARKER_PREFIX);
|
|
68
|
+
if (currentValue !== null) {
|
|
69
|
+
currentCount += 1;
|
|
70
|
+
currentHash = currentValue;
|
|
71
|
+
currentLineIndex = lineIndex;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const legacyValue = extractMarkerValue(line, LEGACY_HASH_MARKER_PREFIX);
|
|
75
|
+
if (legacyValue !== null) {
|
|
76
|
+
legacyCount += 1;
|
|
77
|
+
legacyHash = legacyValue;
|
|
40
78
|
}
|
|
41
79
|
}
|
|
42
|
-
|
|
80
|
+
if (currentCount > 1 || legacyCount > 1 || (currentCount === 1 && legacyCount === 1)) {
|
|
81
|
+
return { kind: 'invalid' };
|
|
82
|
+
}
|
|
83
|
+
if (currentCount === 1) {
|
|
84
|
+
return HEX_HASH_PATTERN.test(currentHash)
|
|
85
|
+
? { kind: 'current', hash: currentHash, lineIndex: currentLineIndex }
|
|
86
|
+
: { kind: 'invalid' };
|
|
87
|
+
}
|
|
88
|
+
if (legacyCount === 1) {
|
|
89
|
+
return LEGACY_HEX_HASH_PATTERN.test(legacyHash) ? { kind: 'legacy' } : { kind: 'invalid' };
|
|
90
|
+
}
|
|
91
|
+
return { kind: 'absent' };
|
|
43
92
|
}
|
|
44
93
|
/** Apply fallback values when knowledge fields are null. */
|
|
45
94
|
function applyFallbacks(knowledge) {
|
|
@@ -53,55 +102,81 @@ function applyFallbacks(knowledge) {
|
|
|
53
102
|
language: knowledge.language || technologyValue('technology-typescript-969545'),
|
|
54
103
|
};
|
|
55
104
|
}
|
|
105
|
+
async function decideForFile(destPath, newDigest, force) {
|
|
106
|
+
let existingContent;
|
|
107
|
+
try {
|
|
108
|
+
existingContent = await readFile(destPath, 'utf-8');
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
const code = err.code;
|
|
112
|
+
if (code === 'ENOENT') {
|
|
113
|
+
return { action: 'write' };
|
|
114
|
+
}
|
|
115
|
+
return { action: 'skip-error', message: err instanceof Error ? err.message : String(err) };
|
|
116
|
+
}
|
|
117
|
+
const marker = readMarker(existingContent);
|
|
118
|
+
switch (marker.kind) {
|
|
119
|
+
case 'invalid':
|
|
120
|
+
case 'absent':
|
|
121
|
+
return { action: 'skip-user-modified' };
|
|
122
|
+
case 'legacy':
|
|
123
|
+
return { action: 'write' };
|
|
124
|
+
case 'current': {
|
|
125
|
+
const onDiskDigest = digestContent(contentWithoutMarkerLine(existingContent, marker.lineIndex));
|
|
126
|
+
if (onDiskDigest !== marker.hash) {
|
|
127
|
+
return { action: 'skip-user-modified' };
|
|
128
|
+
}
|
|
129
|
+
if (force || marker.hash !== newDigest) {
|
|
130
|
+
return { action: 'write' };
|
|
131
|
+
}
|
|
132
|
+
return { action: 'skip-unchanged' };
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
56
136
|
// ---------------------------------------------------------------------------
|
|
57
137
|
// Main export
|
|
58
138
|
// ---------------------------------------------------------------------------
|
|
59
139
|
/**
|
|
60
140
|
* Generate all 22 workflow skill files for the given project.
|
|
61
|
-
*
|
|
62
|
-
*
|
|
141
|
+
* Refreshes a pristine copy when Planu's shipped body changes; never touches
|
|
142
|
+
* a user-authored or user-edited file (SPEC-1457).
|
|
143
|
+
* Writes registry entries via writeSkillRegistryEntry (SPEC-680) only when a
|
|
144
|
+
* file is actually rewritten.
|
|
63
145
|
*/
|
|
64
146
|
export async function generateWorkflowSkills(projectPath, knowledge, options = {}) {
|
|
65
147
|
const targets = options.targets ?? ['claude'];
|
|
66
148
|
const force = options.force ?? false;
|
|
67
149
|
// Apply fallbacks so placeholders always resolve
|
|
68
150
|
const resolvedKnowledge = applyFallbacks(knowledge);
|
|
69
|
-
// Compute current conventions hash (same for all skills in this run)
|
|
70
|
-
const currentHash = hashConventions(resolvedKnowledge);
|
|
71
151
|
// Ensure output directories exist
|
|
72
152
|
const claudeSkillsDir = join(projectPath, CLAUDE_SKILLS_REL);
|
|
73
153
|
await mkdir(claudeSkillsDir, { recursive: true });
|
|
74
154
|
const written = [];
|
|
75
155
|
const skipped = [];
|
|
156
|
+
const userModified = [];
|
|
76
157
|
const errors = {};
|
|
77
158
|
for (const def of WORKFLOW_SKILL_DEFINITIONS) {
|
|
78
159
|
try {
|
|
79
160
|
const skillFileName = `${def.name}.md`;
|
|
80
161
|
const destPath = join(claudeSkillsDir, skillFileName);
|
|
81
|
-
// --- Idempotency check ---
|
|
82
|
-
if (!force) {
|
|
83
|
-
let existingContent = null;
|
|
84
|
-
try {
|
|
85
|
-
existingContent = await readFile(destPath, 'utf-8');
|
|
86
|
-
}
|
|
87
|
-
catch {
|
|
88
|
-
// File does not exist — will write
|
|
89
|
-
}
|
|
90
|
-
if (existingContent !== null) {
|
|
91
|
-
const existingHash = extractExistingHash(existingContent);
|
|
92
|
-
// Scenario 9: missing hash marker → treat as foreign, overwrite
|
|
93
|
-
// Scenario 3: same hash → skip
|
|
94
|
-
if (existingHash !== null && existingHash === currentHash) {
|
|
95
|
-
skipped.push(def.name);
|
|
96
|
-
continue;
|
|
97
|
-
}
|
|
98
|
-
// existingHash === null (missing marker) → fall through and overwrite
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
162
|
// --- Adapt content via SPEC-682 (DRY — no duplicated placeholder logic) ---
|
|
102
163
|
const { adapted } = adaptSkillContent(def.templateBody, resolvedKnowledge);
|
|
164
|
+
const newDigest = digestContent(adapted);
|
|
165
|
+
const decision = await decideForFile(destPath, newDigest, force);
|
|
166
|
+
if (decision.action === 'skip-unchanged') {
|
|
167
|
+
skipped.push(def.name);
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (decision.action === 'skip-user-modified') {
|
|
171
|
+
userModified.push(def.name);
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (decision.action === 'skip-error') {
|
|
175
|
+
errors[def.name] = decision.message;
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
103
178
|
// --- Build final content with hash marker ---
|
|
104
|
-
const finalContent = buildSkillContent(adapted,
|
|
179
|
+
const finalContent = buildSkillContent(adapted, newDigest);
|
|
105
180
|
// --- Write .claude/skills/{name}.md ---
|
|
106
181
|
if (targets.includes('claude')) {
|
|
107
182
|
await writeFile(destPath, finalContent, 'utf-8');
|
|
@@ -121,6 +196,6 @@ export async function generateWorkflowSkills(projectPath, knowledge, options = {
|
|
|
121
196
|
errors[def.name] = err instanceof Error ? err.message : String(err);
|
|
122
197
|
}
|
|
123
198
|
}
|
|
124
|
-
return { written, skipped, errors };
|
|
199
|
+
return { written, skipped, userModified, errors };
|
|
125
200
|
}
|
|
126
201
|
//# sourceMappingURL=workflow-skill-generator.js.map
|
|
@@ -1,16 +1,6 @@
|
|
|
1
1
|
import type { BddScenario, BddStep, LeanCriterion } from '../../types/index.js';
|
|
2
2
|
export type { BddScenario, BddStep };
|
|
3
|
-
|
|
4
|
-
* Parses Gherkin-style blocks from a markdown string.
|
|
5
|
-
*
|
|
6
|
-
* Recognises:
|
|
7
|
-
* Scenario: <title>
|
|
8
|
-
* Given ...
|
|
9
|
-
* When ...
|
|
10
|
-
* Then ...
|
|
11
|
-
* And ...
|
|
12
|
-
* But ...
|
|
13
|
-
*/
|
|
3
|
+
export declare function extractInlineTestPaths(text: string): string[];
|
|
14
4
|
export declare function parseBddScenarios(markdown: string): BddScenario[];
|
|
15
5
|
/**
|
|
16
6
|
* Converts an array of BddScenario objects into YAML lines for the frontmatter.
|
|
@@ -1,4 +1,20 @@
|
|
|
1
1
|
// engine/spec-format/bdd-parser.ts — BDD/Gherkin parser and emitter (SPEC-481)
|
|
2
|
+
import { consumePathRunPreservingCase } from './technical-md-populator.js';
|
|
3
|
+
const TEST_MARKER_RE = /\bTESTS?\s*:/gi;
|
|
4
|
+
export function extractInlineTestPaths(text) {
|
|
5
|
+
const paths = [];
|
|
6
|
+
const seen = new Set();
|
|
7
|
+
for (const marker of text.matchAll(TEST_MARKER_RE)) {
|
|
8
|
+
const start = marker.index + marker[0].length;
|
|
9
|
+
for (const path of consumePathRunPreservingCase(text.slice(start))) {
|
|
10
|
+
if (!seen.has(path)) {
|
|
11
|
+
seen.add(path);
|
|
12
|
+
paths.push(path);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return paths;
|
|
17
|
+
}
|
|
2
18
|
const KEYWORD_MAP = {
|
|
3
19
|
given: 'given',
|
|
4
20
|
when: 'when',
|
|
@@ -17,22 +33,37 @@ const KEYWORD_MAP = {
|
|
|
17
33
|
* And ...
|
|
18
34
|
* But ...
|
|
19
35
|
*/
|
|
36
|
+
const THEN_WORD_RE = /\bTHEN\b/i;
|
|
37
|
+
const WHEN_WORD_RE = /\bWHEN\b/i;
|
|
38
|
+
function hasThenSignal(scenario) {
|
|
39
|
+
return scenario.steps.some((step) => step.keyword === 'then' || THEN_WORD_RE.test(step.text));
|
|
40
|
+
}
|
|
41
|
+
function isSingleLineGivenWhenThenText(text) {
|
|
42
|
+
return WHEN_WORD_RE.test(text) && THEN_WORD_RE.test(text);
|
|
43
|
+
}
|
|
44
|
+
function opensHeaderlessScenario(rawKeyword, text) {
|
|
45
|
+
return rawKeyword === 'given' || isSingleLineGivenWhenThenText(text);
|
|
46
|
+
}
|
|
20
47
|
export function parseBddScenarios(markdown) {
|
|
21
48
|
const scenarios = [];
|
|
22
49
|
const lines = markdown.split('\n');
|
|
23
50
|
let current = null;
|
|
24
51
|
// Tracks whether the current scenario was auto-created (no Scenario: header)
|
|
25
52
|
let currentIsHeaderless = false;
|
|
53
|
+
const closeCurrent = () => {
|
|
54
|
+
if (current && hasThenSignal(current)) {
|
|
55
|
+
scenarios.push(current);
|
|
56
|
+
}
|
|
57
|
+
current = null;
|
|
58
|
+
currentIsHeaderless = false;
|
|
59
|
+
};
|
|
26
60
|
for (const raw of lines) {
|
|
27
61
|
const line = raw.trim();
|
|
28
62
|
// Detect scenario header
|
|
29
63
|
const scenarioMatch = /^scenario:\s*(.+)$/i.exec(line);
|
|
30
64
|
if (scenarioMatch) {
|
|
31
|
-
|
|
32
|
-
scenarios.push(current);
|
|
33
|
-
}
|
|
65
|
+
closeCurrent();
|
|
34
66
|
current = { title: (scenarioMatch[1] ?? '').trim(), steps: [], done: false };
|
|
35
|
-
currentIsHeaderless = false;
|
|
36
67
|
continue;
|
|
37
68
|
}
|
|
38
69
|
// Detect step keywords
|
|
@@ -47,12 +78,12 @@ export function parseBddScenarios(markdown) {
|
|
|
47
78
|
// steps. This handles both multiple headerless blocks and headerless blocks following
|
|
48
79
|
// an explicit Scenario: header block.
|
|
49
80
|
if (rawKeyword === 'given' && current !== null && current.steps.length > 0) {
|
|
50
|
-
|
|
51
|
-
current = null;
|
|
52
|
-
currentIsHeaderless = false;
|
|
81
|
+
closeCurrent();
|
|
53
82
|
}
|
|
54
|
-
// Auto-create a scenario when a step is found without a preceding Scenario header
|
|
55
83
|
if (!current) {
|
|
84
|
+
if (!opensHeaderlessScenario(rawKeyword, text)) {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
56
87
|
current = { title: text, steps: [], done: false };
|
|
57
88
|
currentIsHeaderless = true;
|
|
58
89
|
}
|
|
@@ -62,9 +93,7 @@ export function parseBddScenarios(markdown) {
|
|
|
62
93
|
current.title = text;
|
|
63
94
|
}
|
|
64
95
|
}
|
|
65
|
-
|
|
66
|
-
scenarios.push(current);
|
|
67
|
-
}
|
|
96
|
+
closeCurrent();
|
|
68
97
|
return scenarios;
|
|
69
98
|
}
|
|
70
99
|
/**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// engine/spec-format/lean-spec-generator.ts — Generates lean spec.md with YAML frontmatter (SPEC-461)
|
|
2
2
|
// Output: ~30-50 lines. No generic sections, no OWASP, no STRIDE, no resilience criteria.
|
|
3
|
-
import { parseBddScenarios, renderBddScenariosYaml, convertCheckboxToBdd } from './bdd-parser.js';
|
|
3
|
+
import { parseBddScenarios, renderBddScenariosYaml, convertCheckboxToBdd, extractInlineTestPaths, } from './bdd-parser.js';
|
|
4
4
|
import { decideMetadataSerialization, } from './metadata-value-policy.js';
|
|
5
5
|
import { renderGroundingFrontmatter, renderTechnicalReferenceGroundingFrontmatter, } from '../spec-grounding/contract.js';
|
|
6
6
|
/** Strip a redundant spec-ID prefix from spec titles.
|
|
@@ -117,15 +117,21 @@ function buildBddLines(description, extraCriteria, criteriaOverride, scenarioTes
|
|
|
117
117
|
done: false,
|
|
118
118
|
});
|
|
119
119
|
}
|
|
120
|
+
appendUncoveredCheckboxCriteria(scenarios, description);
|
|
120
121
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
tests:
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
122
|
+
scenarios = scenarios.map((scenario) => {
|
|
123
|
+
const inlinePaths = extractInlineTestPaths([scenario.title, ...scenario.steps.map((step) => step.text)].join('\n'));
|
|
124
|
+
if (inlinePaths.length > 0) {
|
|
125
|
+
return { ...scenario, tests: inlinePaths.map((path) => ({ path })) };
|
|
126
|
+
}
|
|
127
|
+
if (scenario.tests && scenario.tests.length > 0) {
|
|
128
|
+
return scenario;
|
|
129
|
+
}
|
|
130
|
+
if (scenarioTestPaths.length > 0) {
|
|
131
|
+
return { ...scenario, tests: scenarioTestPaths.map((path) => ({ path })) };
|
|
132
|
+
}
|
|
133
|
+
return scenario;
|
|
134
|
+
});
|
|
129
135
|
return renderBddScenariosYaml(scenarios);
|
|
130
136
|
}
|
|
131
137
|
/** Extract acceptance criteria explicitly present in the description.
|
|
@@ -166,7 +172,7 @@ function extractCheckboxCriteria(description) {
|
|
|
166
172
|
function extractGivenWhenThenCriteria(description) {
|
|
167
173
|
const criteria = [];
|
|
168
174
|
// Single-line: GIVEN ... WHEN ... THEN ...
|
|
169
|
-
const singleLineRegex = /^[ \t]*(?:[
|
|
175
|
+
const singleLineRegex = /^[ \t]*(?:[-*+>]\s*|\d+[.)]\s*)?GIVEN\s+.+\s+WHEN\s+.+\s+THEN\s+(.+)$/gim;
|
|
170
176
|
let match = singleLineRegex.exec(description);
|
|
171
177
|
while (match) {
|
|
172
178
|
const thenText = match[1]?.trim() ?? '';
|
|
@@ -214,4 +220,18 @@ function extractPlainListCriteria(description) {
|
|
|
214
220
|
function escapeYaml(value) {
|
|
215
221
|
return value.replace(/"/g, '\\"');
|
|
216
222
|
}
|
|
223
|
+
function normalizeForDedupe(value) {
|
|
224
|
+
return value.trim().toLowerCase();
|
|
225
|
+
}
|
|
226
|
+
function appendUncoveredCheckboxCriteria(scenarios, description) {
|
|
227
|
+
const existingTexts = new Set(scenarios.flatMap((scenario) => [
|
|
228
|
+
normalizeForDedupe(scenario.title),
|
|
229
|
+
...scenario.steps.map((step) => normalizeForDedupe(step.text)),
|
|
230
|
+
]));
|
|
231
|
+
for (const criterion of extractCheckboxCriteria(description)) {
|
|
232
|
+
if (!existingTexts.has(normalizeForDedupe(criterion.text))) {
|
|
233
|
+
scenarios.push(...convertCheckboxToBdd([criterion]));
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
217
237
|
//# sourceMappingURL=lean-spec-generator.js.map
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export const RETIRED_SCAFFOLD_PREFIX = 'GIVEN the requested change is implemented WHEN the behavior is exercised';
|
|
2
|
+
export function isRetiredScaffoldText(text) {
|
|
3
|
+
return text.toLowerCase().includes(RETIRED_SCAFFOLD_PREFIX.toLowerCase());
|
|
4
|
+
}
|
|
5
|
+
//# sourceMappingURL=retired-scaffold.js.map
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { LeanFileEntry } from '../../types/index.js';
|
|
2
|
+
export declare function consumePathRunPreservingCase(remainder: string): string[];
|
|
2
3
|
/**
|
|
3
4
|
* Extract the path list declared immediately after each FILES:/FILE:/TEST:/TESTS: marker.
|
|
4
5
|
* Returns null when no such marker is present (caller falls back to whole-text grounding).
|
|
@@ -137,6 +137,33 @@ function consumePathRun(remainder) {
|
|
|
137
137
|
}
|
|
138
138
|
return paths;
|
|
139
139
|
}
|
|
140
|
+
export function consumePathRunPreservingCase(remainder) {
|
|
141
|
+
const paths = [];
|
|
142
|
+
const seen = new Set();
|
|
143
|
+
let cursor = remainder;
|
|
144
|
+
for (;;) {
|
|
145
|
+
cursor = cursor.replace(PATH_SEPARATOR_RE, '');
|
|
146
|
+
const tokenMatch = PATH_TOKEN_RE.exec(cursor);
|
|
147
|
+
if (tokenMatch?.[1] === undefined) {
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
const raw = tokenMatch[1];
|
|
151
|
+
const endsWithTerminator = /[.;:]+$/.test(raw);
|
|
152
|
+
const trimmed = raw.replace(TRAILING_PUNCTUATION_RE, '');
|
|
153
|
+
if (!isPathShaped(trimmed)) {
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
if (!seen.has(trimmed)) {
|
|
157
|
+
seen.add(trimmed);
|
|
158
|
+
paths.push(trimmed);
|
|
159
|
+
}
|
|
160
|
+
if (endsWithTerminator) {
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
cursor = cursor.slice(tokenMatch[0].length);
|
|
164
|
+
}
|
|
165
|
+
return paths;
|
|
166
|
+
}
|
|
140
167
|
/**
|
|
141
168
|
* Extract the path list declared immediately after each FILES:/FILE:/TEST:/TESTS: marker.
|
|
142
169
|
* Returns null when no such marker is present (caller falls back to whole-text grounding).
|
|
@@ -21,4 +21,5 @@ export declare function containsUnresolvedContractPlaceholder(rawText: string, o
|
|
|
21
21
|
export declare function validateUnifiedSpecCandidate(candidate: string, options?: {
|
|
22
22
|
groundedFilePaths?: Iterable<string>;
|
|
23
23
|
}): UnifiedSpecCandidateValidation;
|
|
24
|
+
export declare function maskFencedAndQuotedText(body: string): string;
|
|
24
25
|
//# sourceMappingURL=unified-spec-builder.d.ts.map
|