cclaw-cli 0.48.27 → 0.48.29

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.
@@ -1,10 +1,11 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
+ import { resolveArtifactPath as resolveStageArtifactPath } from "./artifact-paths.js";
3
4
  import { RUNTIME_ROOT, SHIP_FINALIZATION_MODES } from "./constants.js";
4
5
  import { exists } from "./fs-utils.js";
5
6
  import { stageSchema } from "./content/stage-schema.js";
6
7
  import { FLOW_STAGES } from "./types.js";
7
- async function resolveArtifactPath(projectRoot, fileName) {
8
+ async function resolveNamedArtifactPath(projectRoot, fileName) {
8
9
  const relPath = path.join(RUNTIME_ROOT, "artifacts", fileName);
9
10
  const absPath = path.join(projectRoot, relPath);
10
11
  return { absPath, relPath };
@@ -791,7 +792,11 @@ function validateSectionBody(sectionBody, rule, sectionName) {
791
792
  }
792
793
  export async function lintArtifact(projectRoot, stage, track = "standard") {
793
794
  const schema = stageSchema(stage, track);
794
- const { absPath: absFile, relPath: relFile } = await resolveArtifactPath(projectRoot, schema.artifactFile);
795
+ const { absPath: absFile, relPath: relFile } = await resolveStageArtifactPath(stage, {
796
+ projectRoot,
797
+ track,
798
+ intent: "read"
799
+ });
795
800
  const findings = [];
796
801
  if (!(await exists(absFile))) {
797
802
  for (const v of schema.artifactValidation) {
@@ -949,8 +954,14 @@ export async function lintArtifact(projectRoot, stage, track = "standard") {
949
954
  ? "No placeholder tokens detected in Task List."
950
955
  : `Detected placeholder token(s) in Task List: ${placeholderHits.join(", ")}.`
951
956
  });
952
- const scopePath = path.join(projectRoot, RUNTIME_ROOT, "artifacts", "02-scope.md");
953
- const scopeRaw = (await exists(scopePath)) ? await fs.readFile(scopePath, "utf8") : "";
957
+ const scopeArtifact = await resolveStageArtifactPath("scope", {
958
+ projectRoot,
959
+ track,
960
+ intent: "read"
961
+ });
962
+ const scopeRaw = (await exists(scopeArtifact.absPath))
963
+ ? await fs.readFile(scopeArtifact.absPath, "utf8")
964
+ : "";
954
965
  const scopeDecisionIds = extractDecisionIds(scopeRaw);
955
966
  const missingDecisionRefs = scopeDecisionIds.filter((id) => !raw.includes(id));
956
967
  findings.push({
@@ -1053,7 +1064,7 @@ function isStringArray(v) {
1053
1064
  }
1054
1065
  export async function validateReviewArmy(projectRoot) {
1055
1066
  const errors = [];
1056
- const { absPath, relPath } = await resolveArtifactPath(projectRoot, "07-review-army.json");
1067
+ const { absPath, relPath } = await resolveNamedArtifactPath(projectRoot, "07-review-army.json");
1057
1068
  if (!(await exists(absPath))) {
1058
1069
  return { valid: false, errors: [`Missing file: ${relPath}`] };
1059
1070
  }
@@ -0,0 +1,28 @@
1
+ import type { FlowStage, FlowTrack } from "./types.js";
2
+ export type ArtifactPathIntent = "read" | "write";
3
+ export interface ResolveArtifactPathContext {
4
+ projectRoot: string;
5
+ track?: FlowTrack;
6
+ /**
7
+ * Optional brainstorm topic used for `<slug>` interpolation.
8
+ * When omitted, the resolver attempts to infer it from `00-idea.md`.
9
+ */
10
+ topic?: string;
11
+ /**
12
+ * - read: locate an existing artifact first (new slug shape, then legacy fallback).
13
+ * - write: return a non-colliding writable path for a new artifact.
14
+ */
15
+ intent?: ArtifactPathIntent;
16
+ }
17
+ export interface ResolvedArtifactPath {
18
+ stage: FlowStage;
19
+ fileName: string;
20
+ relPath: string;
21
+ absPath: string;
22
+ source: "existing" | "generated";
23
+ legacy: boolean;
24
+ }
25
+ export declare function isSlugArtifactPattern(filePattern: string): boolean;
26
+ export declare function legacyArtifactFileName(filePattern: string): string;
27
+ export declare function slugifyArtifactTopic(topic: string): string;
28
+ export declare function resolveArtifactPath(stage: FlowStage, context: ResolveArtifactPathContext): Promise<ResolvedArtifactPath>;
@@ -0,0 +1,261 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { stageSchema } from "./content/stage-schema.js";
4
+ import { RUNTIME_ROOT } from "./constants.js";
5
+ import { exists } from "./fs-utils.js";
6
+ const LEGACY_ARTIFACT_GRACE_CYCLES = 2;
7
+ const DEFAULT_TOPIC_SLUG = "topic";
8
+ function escapeRegExp(value) {
9
+ return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
10
+ }
11
+ function splitExt(fileName) {
12
+ const ext = path.extname(fileName);
13
+ if (!ext) {
14
+ return { stem: fileName, ext: "" };
15
+ }
16
+ return { stem: fileName.slice(0, -ext.length), ext };
17
+ }
18
+ function appendCollisionSuffix(fileName, index) {
19
+ const { stem, ext } = splitExt(fileName);
20
+ return `${stem}-${index}${ext}`;
21
+ }
22
+ export function isSlugArtifactPattern(filePattern) {
23
+ return filePattern.includes("<slug>");
24
+ }
25
+ export function legacyArtifactFileName(filePattern) {
26
+ if (!isSlugArtifactPattern(filePattern)) {
27
+ return filePattern;
28
+ }
29
+ return filePattern.replace(/-<slug>/gu, "");
30
+ }
31
+ export function slugifyArtifactTopic(topic) {
32
+ const normalized = topic
33
+ .toLowerCase()
34
+ .trim()
35
+ .replace(/[`"'“”‘’()[\]{}<>]/gu, " ")
36
+ .replace(/[^a-z0-9]+/gu, "-")
37
+ .replace(/^-+/u, "")
38
+ .replace(/-+$/u, "");
39
+ if (normalized.length === 0) {
40
+ return DEFAULT_TOPIC_SLUG;
41
+ }
42
+ return normalized.slice(0, 48);
43
+ }
44
+ function slugPatternRegex(filePattern) {
45
+ const [left, right] = filePattern.split("<slug>");
46
+ return new RegExp(`^${escapeRegExp(left ?? "")}[a-z0-9]+(?:-[a-z0-9]+)*(?:-\\d+)?${escapeRegExp(right ?? "")}$`, "u");
47
+ }
48
+ function searchRoots(projectRoot) {
49
+ return [
50
+ {
51
+ absDir: path.join(projectRoot, RUNTIME_ROOT, "artifacts"),
52
+ relPrefix: path.join(RUNTIME_ROOT, "artifacts")
53
+ },
54
+ {
55
+ absDir: projectRoot,
56
+ relPrefix: ""
57
+ }
58
+ ];
59
+ }
60
+ function candidateFromRoot(root, fileName) {
61
+ return {
62
+ relPath: root.relPrefix ? path.join(root.relPrefix, fileName) : fileName,
63
+ absPath: path.join(root.absDir, fileName)
64
+ };
65
+ }
66
+ async function inferTopicFromIdeaArtifact(projectRoot) {
67
+ const ideaPath = path.join(projectRoot, RUNTIME_ROOT, "artifacts", "00-idea.md");
68
+ if (!(await exists(ideaPath))) {
69
+ return null;
70
+ }
71
+ try {
72
+ const raw = await fs.readFile(ideaPath, "utf8");
73
+ const lines = raw.split(/\r?\n/gu);
74
+ const userPromptHeading = lines.findIndex((line) => /^##\s+user prompt\b/iu.test(line.trim()));
75
+ if (userPromptHeading >= 0) {
76
+ for (let i = userPromptHeading + 1; i < lines.length; i += 1) {
77
+ const line = lines[i].trim();
78
+ if (line.length === 0)
79
+ continue;
80
+ if (/^##\s+/u.test(line))
81
+ break;
82
+ const candidate = line.replace(/^[-*>\s#]+/u, "").trim();
83
+ if (candidate.length > 0) {
84
+ return candidate;
85
+ }
86
+ }
87
+ }
88
+ const metadataLine = /^(?:class|track|stack|reclassification)\s*:/iu;
89
+ for (const line of lines) {
90
+ const trimmed = line.trim();
91
+ if (trimmed.length === 0)
92
+ continue;
93
+ if (metadataLine.test(trimmed))
94
+ continue;
95
+ if (/^##\s+/u.test(trimmed))
96
+ continue;
97
+ const candidate = trimmed.replace(/^[-*>\s#]+/u, "").trim();
98
+ if (candidate.length > 0) {
99
+ return candidate;
100
+ }
101
+ }
102
+ return null;
103
+ }
104
+ catch {
105
+ return null;
106
+ }
107
+ }
108
+ async function resolvedTopicSlug(projectRoot, stage, explicitTopic) {
109
+ if (explicitTopic && explicitTopic.trim().length > 0) {
110
+ return slugifyArtifactTopic(explicitTopic);
111
+ }
112
+ const inferred = await inferTopicFromIdeaArtifact(projectRoot);
113
+ if (inferred && inferred.trim().length > 0) {
114
+ return slugifyArtifactTopic(inferred);
115
+ }
116
+ return slugifyArtifactTopic(stage);
117
+ }
118
+ async function collectExistingCandidates(projectRoot, filePattern, legacyFile) {
119
+ const roots = searchRoots(projectRoot);
120
+ const candidates = [];
121
+ const hasSlugPattern = isSlugArtifactPattern(filePattern);
122
+ const matcher = hasSlugPattern ? slugPatternRegex(filePattern) : null;
123
+ for (const root of roots) {
124
+ if (hasSlugPattern && matcher) {
125
+ let entries = [];
126
+ try {
127
+ entries = await fs.readdir(root.absDir);
128
+ }
129
+ catch {
130
+ entries = [];
131
+ }
132
+ for (const entry of entries) {
133
+ if (!matcher.test(entry))
134
+ continue;
135
+ const { relPath, absPath } = candidateFromRoot(root, entry);
136
+ let mtimeMs = 0;
137
+ try {
138
+ const stat = await fs.stat(absPath);
139
+ mtimeMs = stat.mtimeMs;
140
+ }
141
+ catch {
142
+ continue;
143
+ }
144
+ candidates.push({
145
+ fileName: entry,
146
+ relPath,
147
+ absPath,
148
+ mtimeMs,
149
+ legacy: false
150
+ });
151
+ }
152
+ }
153
+ else {
154
+ const { relPath, absPath } = candidateFromRoot(root, filePattern);
155
+ if (await exists(absPath)) {
156
+ let mtimeMs = 0;
157
+ try {
158
+ const stat = await fs.stat(absPath);
159
+ mtimeMs = stat.mtimeMs;
160
+ }
161
+ catch {
162
+ mtimeMs = 0;
163
+ }
164
+ candidates.push({
165
+ fileName: filePattern,
166
+ relPath,
167
+ absPath,
168
+ mtimeMs,
169
+ legacy: false
170
+ });
171
+ }
172
+ }
173
+ if (legacyFile && LEGACY_ARTIFACT_GRACE_CYCLES > 0) {
174
+ const { relPath, absPath } = candidateFromRoot(root, legacyFile);
175
+ if (await exists(absPath)) {
176
+ let mtimeMs = 0;
177
+ try {
178
+ const stat = await fs.stat(absPath);
179
+ mtimeMs = stat.mtimeMs;
180
+ }
181
+ catch {
182
+ mtimeMs = 0;
183
+ }
184
+ candidates.push({
185
+ fileName: legacyFile,
186
+ relPath,
187
+ absPath,
188
+ mtimeMs,
189
+ legacy: true
190
+ });
191
+ }
192
+ }
193
+ }
194
+ candidates.sort((a, b) => {
195
+ if (b.mtimeMs !== a.mtimeMs) {
196
+ return b.mtimeMs - a.mtimeMs;
197
+ }
198
+ if (a.legacy !== b.legacy) {
199
+ return a.legacy ? 1 : -1;
200
+ }
201
+ return a.fileName.localeCompare(b.fileName);
202
+ });
203
+ return candidates;
204
+ }
205
+ export async function resolveArtifactPath(stage, context) {
206
+ const track = context.track ?? "standard";
207
+ const intent = context.intent ?? "read";
208
+ const filePattern = stageSchema(stage, track).artifactFile;
209
+ const hasSlugPattern = isSlugArtifactPattern(filePattern);
210
+ const legacyFile = hasSlugPattern ? legacyArtifactFileName(filePattern) : null;
211
+ const existing = await collectExistingCandidates(context.projectRoot, filePattern, legacyFile);
212
+ if (intent === "read" && existing.length > 0) {
213
+ const picked = existing[0];
214
+ return {
215
+ stage,
216
+ fileName: picked.fileName,
217
+ relPath: picked.relPath,
218
+ absPath: picked.absPath,
219
+ source: "existing",
220
+ legacy: picked.legacy
221
+ };
222
+ }
223
+ const artifactRoot = path.join(context.projectRoot, RUNTIME_ROOT, "artifacts");
224
+ if (!hasSlugPattern) {
225
+ return {
226
+ stage,
227
+ fileName: filePattern,
228
+ relPath: path.join(RUNTIME_ROOT, "artifacts", filePattern),
229
+ absPath: path.join(artifactRoot, filePattern),
230
+ source: "generated",
231
+ legacy: false
232
+ };
233
+ }
234
+ const topicSlug = await resolvedTopicSlug(context.projectRoot, stage, context.topic);
235
+ const baseFileName = filePattern.replace("<slug>", topicSlug);
236
+ if (intent === "read") {
237
+ return {
238
+ stage,
239
+ fileName: baseFileName,
240
+ relPath: path.join(RUNTIME_ROOT, "artifacts", baseFileName),
241
+ absPath: path.join(artifactRoot, baseFileName),
242
+ source: "generated",
243
+ legacy: false
244
+ };
245
+ }
246
+ let candidate = baseFileName;
247
+ let index = 2;
248
+ // Keep incrementing while a matching file exists under active artifacts root.
249
+ while (await exists(path.join(artifactRoot, candidate))) {
250
+ candidate = appendCollisionSuffix(baseFileName, index);
251
+ index += 1;
252
+ }
253
+ return {
254
+ stage,
255
+ fileName: candidate,
256
+ relPath: path.join(RUNTIME_ROOT, "artifacts", candidate),
257
+ absPath: path.join(artifactRoot, candidate),
258
+ source: "generated",
259
+ legacy: false
260
+ };
261
+ }
@@ -1,4 +1,4 @@
1
- import { stageSchema } from "./stage-schema.js";
1
+ import { stagePolicyNeedles, stageSchema } from "./stage-schema.js";
2
2
  import { stageSkillFolder } from "./skills.js";
3
3
  export function stageCommandContract(stage, track = "standard") {
4
4
  const schema = stageSchema(stage, track);
@@ -17,6 +17,7 @@ export function stageCommandContract(stage, track = "standard") {
17
17
  const writeStepPaths = writes.length > 1
18
18
  ? writes.map((w) => `\`${w}\``).join(" and ")
19
19
  : `\`${primaryArtifact}\``;
20
+ const policyNeedles = stagePolicyNeedles(stage, track);
20
21
  return `# /cc-${stage}
21
22
 
22
23
  Load and follow **${skillPath}** — it contains the full checklist, examples, interaction protocol, and verification discipline.
@@ -45,6 +46,6 @@ ${gateIds}
45
46
  ${schema.exitCriteria.map((v) => `- ${v}`).join("\n")}
46
47
 
47
48
  ## Anchors
48
- ${schema.policyNeedles.map((v) => `- ${v}`).join("\n")}
49
+ ${policyNeedles.map((v) => `- ${v}`).join("\n")}
49
50
  `;
50
51
  }
@@ -1,6 +1,6 @@
1
1
  import type { FlowStage, FlowTrack, TransitionRule } from "../types.js";
2
2
  import type { StageComplexityTier, StageAutoSubagentDispatch, StageSchema } from "./stages/schema-types.js";
3
- export type { ArtifactValidation, CrossStageTrace, ReviewSection, StageComplexityTier, StageExecutionModel, StagePhilosophy, StageArtifactRules, StageReviewLens, StageAutoSubagentDispatch, StageGate, StageSchema, StageSchemaInput } from "./stages/schema-types.js";
3
+ export type { ArtifactValidation, CrossStageTrace, ReviewSection, StageComplexityTier, StageExecutionModel, StagePhilosophy, StageArtifactRules, StageReviewLens, StageAutoSubagentDispatch, StageGate, StageSchemaLegacyInput, StageSchema, StageSchemaInput, StageSchemaV2Input } from "./stages/schema-types.js";
4
4
  export declare const SKILL_ENVELOPE_KINDS: readonly ["stage-output", "gate-result", "delegation-record"];
5
5
  export type SkillEnvelopeKind = (typeof SKILL_ENVELOPE_KINDS)[number];
6
6
  export interface SkillEnvelope {
@@ -1,6 +1,7 @@
1
1
  import { FLOW_STAGES, FLOW_TRACKS, TRACK_STAGES } from "../types.js";
2
2
  import { STAGE_TO_SKILL_FOLDER } from "../constants.js";
3
3
  import { BRAINSTORM, SCOPE, DESIGN, SPEC, PLAN, TDD, REVIEW, SHIP } from "./stages/index.js";
4
+ import { stagePolicyNeedlesFromMetadata } from "./stages/_lint-metadata/index.js";
4
5
  import { tddStageForTrack } from "./stages/tdd.js";
5
6
  // ---------------------------------------------------------------------------
6
7
  // NOTE: The former QUESTION_FORMAT_SPEC / ERROR_BUDGET_SPEC exports were
@@ -66,17 +67,32 @@ export function parseSkillEnvelope(raw) {
66
67
  }
67
68
  return parsed;
68
69
  }
69
- const ARTIFACT_STAGE_BY_PATH = {
70
- ".cclaw/artifacts/01-brainstorm.md": "brainstorm",
71
- ".cclaw/artifacts/02-scope.md": "scope",
72
- ".cclaw/artifacts/02a-research.md": "design",
73
- ".cclaw/artifacts/03-design.md": "design",
74
- ".cclaw/artifacts/04-spec.md": "spec",
75
- ".cclaw/artifacts/05-plan.md": "plan",
76
- ".cclaw/artifacts/06-tdd.md": "tdd",
77
- ".cclaw/artifacts/07-review.md": "review",
78
- ".cclaw/artifacts/08-ship.md": "ship"
70
+ const ARTIFACT_STAGE_BY_PREFIX = {
71
+ "01": "brainstorm",
72
+ "02": "scope",
73
+ "03": "design",
74
+ "04": "spec",
75
+ "05": "plan",
76
+ "06": "tdd",
77
+ "07": "review",
78
+ "08": "ship"
79
79
  };
80
+ const ARTIFACT_STAGE_BY_SPECIAL_FILE = {
81
+ "02a-research.md": "design"
82
+ };
83
+ function stageFromArtifactPath(artifactPath) {
84
+ const normalized = artifactPath.replace(/\\/gu, "/");
85
+ const fileName = normalized.split("/").pop() ?? normalized;
86
+ const special = ARTIFACT_STAGE_BY_SPECIAL_FILE[fileName];
87
+ if (special) {
88
+ return special;
89
+ }
90
+ const match = /^(\d{2})(?:[a-z])?-/u.exec(fileName);
91
+ if (!match) {
92
+ return null;
93
+ }
94
+ return ARTIFACT_STAGE_BY_PREFIX[match[1]] ?? null;
95
+ }
80
96
  const REQUIRED_GATE_IDS = {
81
97
  brainstorm: [
82
98
  "brainstorm_approaches_compared",
@@ -171,13 +187,53 @@ function tieredArtifactValidation(stage, rows) {
171
187
  function readsFromForTrack(readsFrom, track) {
172
188
  const stageSet = new Set(TRACK_STAGES[track]);
173
189
  return readsFrom.filter((artifactPath) => {
174
- const stage = ARTIFACT_STAGE_BY_PATH[artifactPath];
190
+ const stage = stageFromArtifactPath(artifactPath);
175
191
  if (!stage) {
176
192
  return true;
177
193
  }
178
194
  return stageSet.has(stage);
179
195
  });
180
196
  }
197
+ function isStageSchemaV2Input(value) {
198
+ return value.schemaShape === "v2";
199
+ }
200
+ function normalizeStageSchemaInput(value) {
201
+ if (!isStageSchemaV2Input(value)) {
202
+ return value;
203
+ }
204
+ return {
205
+ stage: value.stage,
206
+ skillFolder: value.skillFolder,
207
+ skillName: value.skillName,
208
+ skillDescription: value.skillDescription,
209
+ complexityTier: value.complexityTier,
210
+ hardGate: value.philosophy.hardGate,
211
+ ironLaw: value.philosophy.ironLaw,
212
+ purpose: value.philosophy.purpose,
213
+ whenToUse: value.philosophy.whenToUse,
214
+ whenNotToUse: value.philosophy.whenNotToUse,
215
+ interactionProtocol: value.executionModel.interactionProtocol,
216
+ process: value.executionModel.process,
217
+ requiredGates: value.executionModel.requiredGates,
218
+ requiredEvidence: value.executionModel.requiredEvidence,
219
+ inputs: value.executionModel.inputs,
220
+ requiredContext: value.executionModel.requiredContext,
221
+ researchPlaybooks: value.executionModel.researchPlaybooks,
222
+ outputs: value.reviewLens.outputs,
223
+ blockers: value.executionModel.blockers,
224
+ exitCriteria: value.executionModel.exitCriteria,
225
+ commonRationalizations: value.philosophy.commonRationalizations,
226
+ artifactFile: value.artifactRules.artifactFile,
227
+ next: value.next,
228
+ checklist: value.executionModel.checklist,
229
+ reviewSections: value.reviewLens.reviewSections,
230
+ completionStatus: value.artifactRules.completionStatus,
231
+ crossStageTrace: value.artifactRules.crossStageTrace,
232
+ artifactValidation: value.artifactRules.artifactValidation,
233
+ batchExecutionAllowed: value.batchExecutionAllowed,
234
+ trivialOverrideSections: value.artifactRules.trivialOverrideSections
235
+ };
236
+ }
181
237
  // ---------------------------------------------------------------------------
182
238
  // Stage map and accessors
183
239
  // ---------------------------------------------------------------------------
@@ -357,7 +413,8 @@ export function mandatoryDelegationsForStage(stage, complexityTier = "standard")
357
413
  .map((d) => d.agent))];
358
414
  }
359
415
  export function stageSchema(stage, track = "standard") {
360
- const base = stage === "tdd" ? tddStageForTrack(track) : STAGE_SCHEMA_MAP[stage];
416
+ const rawInput = stage === "tdd" ? tddStageForTrack(track) : STAGE_SCHEMA_MAP[stage];
417
+ const base = normalizeStageSchemaInput(rawInput);
361
418
  const tieredGates = tieredStageGates(stage, base.requiredGates, track);
362
419
  const tieredValidation = tieredArtifactValidation(stage, base.artifactValidation);
363
420
  const crossStageTrace = {
@@ -396,8 +453,7 @@ export function stageSchema(stage, track = "standard") {
396
453
  const reviewLens = {
397
454
  outputs: base.outputs,
398
455
  reviewSections: base.reviewSections,
399
- mandatoryDelegations,
400
- policyNeedles: base.policyNeedles
456
+ mandatoryDelegations
401
457
  };
402
458
  return {
403
459
  ...base,
@@ -464,7 +520,7 @@ export function buildTransitionRules() {
464
520
  return rules;
465
521
  }
466
522
  export function stagePolicyNeedles(stage, track = "standard") {
467
- return stageSchema(stage, track).policyNeedles;
523
+ return stagePolicyNeedlesFromMetadata(stage, track);
468
524
  }
469
525
  export function stageAutoSubagentDispatch(stage) {
470
526
  return STAGE_AUTO_SUBAGENT_DISPATCH[stage];
@@ -0,0 +1,2 @@
1
+ import type { FlowStage, FlowTrack } from "../../../types.js";
2
+ export declare function stagePolicyNeedlesFromMetadata(stage: FlowStage, track?: FlowTrack): string[];
@@ -0,0 +1,79 @@
1
+ import { SHIP_FINALIZATION_MODES } from "../../../constants.js";
2
+ const STAGE_POLICY_NEEDLES = {
3
+ brainstorm: [
4
+ "Explore project context",
5
+ "One question at a time",
6
+ "2-3 architecturally distinct approaches",
7
+ "State what is being approved",
8
+ "Self-review before handoff",
9
+ "Do NOT implement, scaffold, or modify behavior"
10
+ ],
11
+ scope: [
12
+ "Scope mode",
13
+ "In Scope",
14
+ "Out of Scope",
15
+ "Discretion Areas",
16
+ "NOT in scope",
17
+ "Premise Challenge",
18
+ "Locked Decisions"
19
+ ],
20
+ design: [
21
+ "Parallel Research Fleet",
22
+ "Architecture",
23
+ "Data Flow",
24
+ "Failure Modes and Mitigation",
25
+ "Performance Budget",
26
+ "One issue at a time"
27
+ ],
28
+ spec: ["Acceptance Criteria", "Constraints", "Testability", "approved spec", "Edge Cases"],
29
+ plan: [
30
+ "WAIT_FOR_CONFIRM",
31
+ "Task Graph",
32
+ "Dependency Batches",
33
+ "Acceptance Mapping",
34
+ "verification steps",
35
+ "Locked Decision Coverage"
36
+ ],
37
+ tdd: [
38
+ "RED",
39
+ "GREEN",
40
+ "REFACTOR",
41
+ "failing test",
42
+ "full test suite",
43
+ "acceptance criteria",
44
+ "traceable to plan slice"
45
+ ],
46
+ review: [
47
+ "Layer 1",
48
+ "Layer 2",
49
+ "Critical",
50
+ "Review Army",
51
+ "Ready to Ship",
52
+ "ROUTE_BACK_TO_TDD",
53
+ "One issue at a time"
54
+ ],
55
+ ship: [
56
+ "Pre-Ship Checks",
57
+ "Release Notes",
58
+ "Rollback Plan",
59
+ ...SHIP_FINALIZATION_MODES
60
+ ]
61
+ };
62
+ function quickTrackText(value) {
63
+ return value
64
+ .replace(/\btask from the plan\b/giu, "acceptance criterion from the spec")
65
+ .replace(/\bplan task ID\b/giu, "acceptance criterion ID")
66
+ .replace(/\bplan task\b/giu, "acceptance criterion")
67
+ .replace(/\bplan row\b/giu, "acceptance row")
68
+ .replace(/\bplan slice\b/giu, "acceptance slice")
69
+ .replace(/\bplan artifact\b/giu, "spec artifact")
70
+ .replace(/\btraceable to plan slice\b/giu, "traceable to acceptance criterion")
71
+ .replace(/05-plan\.md/gu, "04-spec.md");
72
+ }
73
+ export function stagePolicyNeedlesFromMetadata(stage, track = "standard") {
74
+ const needles = STAGE_POLICY_NEEDLES[stage];
75
+ if (stage === "tdd" && track === "quick") {
76
+ return needles.map(quickTrackText);
77
+ }
78
+ return [...needles];
79
+ }