create-quiver 0.12.0 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +49 -17
  3. package/README_FOR_AI.md +31 -29
  4. package/ROADMAP.md +15 -3
  5. package/docs/AI_ONBOARDING_PROMPT.md.template +7 -1
  6. package/docs/COMMANDS.md.template +44 -18
  7. package/docs/STATUS.md.template +5 -1
  8. package/docs/WORKFLOW.md.template +13 -11
  9. package/package.json +9 -3
  10. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/EVIDENCE_REPORT.md +293 -0
  11. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/EXECUTION_PLAN.md +58 -0
  12. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/SPEC.md +242 -0
  13. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/STATUS.md +35 -0
  14. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/pr.md +77 -0
  15. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-00-spec-foundation/CLOSURE_BRIEF.md +34 -0
  16. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-00-spec-foundation/EXECUTION_BRIEF.md +52 -0
  17. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-00-spec-foundation/slice.json +52 -0
  18. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-01-cli-contract-compatibility/CLOSURE_BRIEF.md +36 -0
  19. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-01-cli-contract-compatibility/EXECUTION_BRIEF.md +52 -0
  20. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-01-cli-contract-compatibility/slice.json +56 -0
  21. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-02-run-state-phase-locks/CLOSURE_BRIEF.md +43 -0
  22. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-02-run-state-phase-locks/EXECUTION_BRIEF.md +54 -0
  23. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-02-run-state-phase-locks/slice.json +52 -0
  24. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-03-safe-ai-onboarding-docs/CLOSURE_BRIEF.md +35 -0
  25. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-03-safe-ai-onboarding-docs/EXECUTION_BRIEF.md +53 -0
  26. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-03-safe-ai-onboarding-docs/slice.json +54 -0
  27. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-04-agent-profiles-adapters/CLOSURE_BRIEF.md +34 -0
  28. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-04-agent-profiles-adapters/EXECUTION_BRIEF.md +54 -0
  29. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-04-agent-profiles-adapters/slice.json +52 -0
  30. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-05-approval-gates/CLOSURE_BRIEF.md +34 -0
  31. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-05-approval-gates/EXECUTION_BRIEF.md +54 -0
  32. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-05-approval-gates/slice.json +53 -0
  33. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-06-spec-slice-generator/CLOSURE_BRIEF.md +33 -0
  34. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-06-spec-slice-generator/EXECUTION_BRIEF.md +56 -0
  35. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-06-spec-slice-generator/slice.json +55 -0
  36. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-07-slice-execution-planner/CLOSURE_BRIEF.md +33 -0
  37. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-07-slice-execution-planner/EXECUTION_BRIEF.md +54 -0
  38. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-07-slice-execution-planner/slice.json +52 -0
  39. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-08-controlled-slice-execution/CLOSURE_BRIEF.md +39 -0
  40. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-08-controlled-slice-execution/EXECUTION_BRIEF.md +56 -0
  41. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-08-controlled-slice-execution/slice.json +53 -0
  42. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-09-git-worktree-pr-lifecycle/CLOSURE_BRIEF.md +38 -0
  43. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-09-git-worktree-pr-lifecycle/EXECUTION_BRIEF.md +57 -0
  44. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-09-git-worktree-pr-lifecycle/slice.json +52 -0
  45. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-10-validation-errors-fixtures/CLOSURE_BRIEF.md +39 -0
  46. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-10-validation-errors-fixtures/EXECUTION_BRIEF.md +55 -0
  47. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-10-validation-errors-fixtures/slice.json +56 -0
  48. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-11-export-dashboard-migration/CLOSURE_BRIEF.md +36 -0
  49. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-11-export-dashboard-migration/EXECUTION_BRIEF.md +54 -0
  50. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-11-export-dashboard-migration/slice.json +53 -0
  51. package/specs/quiver-v26-0121-smoke-hardening/EVIDENCE_REPORT.md +208 -0
  52. package/specs/quiver-v26-0121-smoke-hardening/EXECUTION_PLAN.md +57 -0
  53. package/specs/quiver-v26-0121-smoke-hardening/SPEC.md +137 -0
  54. package/specs/quiver-v26-0121-smoke-hardening/STATUS.md +32 -0
  55. package/specs/quiver-v26-0121-smoke-hardening/pr.md +96 -0
  56. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-00-docs-foundation/CLOSURE_BRIEF.md +35 -0
  57. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-00-docs-foundation/EXECUTION_BRIEF.md +55 -0
  58. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-00-docs-foundation/slice.json +73 -0
  59. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-01-cli-help-version-contract/CLOSURE_BRIEF.md +38 -0
  60. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-01-cli-help-version-contract/EXECUTION_BRIEF.md +51 -0
  61. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-01-cli-help-version-contract/slice.json +76 -0
  62. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-02-init-doc-links-and-flow-guidance/CLOSURE_BRIEF.md +37 -0
  63. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-02-init-doc-links-and-flow-guidance/EXECUTION_BRIEF.md +52 -0
  64. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-02-init-doc-links-and-flow-guidance/slice.json +75 -0
  65. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-03-ai-approval-review-consistency/CLOSURE_BRIEF.md +37 -0
  66. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-03-ai-approval-review-consistency/EXECUTION_BRIEF.md +53 -0
  67. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-03-ai-approval-review-consistency/slice.json +77 -0
  68. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-04-local-validation-brief-contracts/CLOSURE_BRIEF.md +35 -0
  69. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-04-local-validation-brief-contracts/EXECUTION_BRIEF.md +52 -0
  70. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-04-local-validation-brief-contracts/slice.json +77 -0
  71. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-05-demo-scaffold-readiness/CLOSURE_BRIEF.md +34 -0
  72. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-05-demo-scaffold-readiness/EXECUTION_BRIEF.md +54 -0
  73. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-05-demo-scaffold-readiness/slice.json +84 -0
  74. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-06-plan-graph-scope-performance/CLOSURE_BRIEF.md +35 -0
  75. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-06-plan-graph-scope-performance/EXECUTION_BRIEF.md +53 -0
  76. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-06-plan-graph-scope-performance/slice.json +82 -0
  77. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-07-smoke-release-readiness/CLOSURE_BRIEF.md +35 -0
  78. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-07-smoke-release-readiness/EXECUTION_BRIEF.md +55 -0
  79. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-07-smoke-release-readiness/slice.json +92 -0
  80. package/src/create-quiver/commands/ai.js +577 -27
  81. package/src/create-quiver/commands/flow.js +6 -5
  82. package/src/create-quiver/commands/graph.js +6 -4
  83. package/src/create-quiver/commands/plan.js +3 -3
  84. package/src/create-quiver/index.js +328 -12
  85. package/src/create-quiver/lib/actionable-error.js +27 -0
  86. package/src/create-quiver/lib/agent-profiles.js +1 -1
  87. package/src/create-quiver/lib/ai/context-packs.js +4 -0
  88. package/src/create-quiver/lib/ai/execution-plan.js +7 -1
  89. package/src/create-quiver/lib/ai/executor.js +270 -20
  90. package/src/create-quiver/lib/ai/export-state.js +534 -0
  91. package/src/create-quiver/lib/ai/github.js +83 -0
  92. package/src/create-quiver/lib/ai/onboarding-template.js +215 -2
  93. package/src/create-quiver/lib/ai/plan-review.js +5 -2
  94. package/src/create-quiver/lib/ai/providers.js +4 -3
  95. package/src/create-quiver/lib/ai/run-state.js +414 -0
  96. package/src/create-quiver/lib/ai/spec-generator.js +12 -0
  97. package/src/create-quiver/lib/ai/spec-templates.js +78 -9
  98. package/src/create-quiver/lib/approvals.js +22 -3
  99. package/src/create-quiver/lib/demo.js +189 -14
  100. package/src/create-quiver/lib/doctor.js +75 -0
  101. package/src/create-quiver/lib/handoff.js +81 -12
  102. package/src/create-quiver/lib/init-docs.js +24 -6
  103. package/src/create-quiver/lib/init-layout.js +8 -0
  104. package/src/create-quiver/lib/json.js +53 -3
  105. package/src/create-quiver/lib/readiness.js +18 -3
  106. package/src/create-quiver/lib/scope.js +50 -7
  107. package/src/create-quiver/lib/slice-graph.js +138 -38
  108. package/src/create-quiver/lib/slice.js +6 -1
  109. package/src/create-quiver/lib/spec-worktrees.js +16 -2
@@ -1,7 +1,57 @@
1
1
  function stripJsonComments(text) {
2
- return String(text || '')
3
- .replace(/^\s*\/\/.*$/gm, '')
4
- .replace(/\/\*[\s\S]*?\*\//g, '');
2
+ const input = String(text || '');
3
+ let output = '';
4
+ let inString = false;
5
+ let escaped = false;
6
+
7
+ for (let index = 0; index < input.length; index += 1) {
8
+ const char = input[index];
9
+ const next = input[index + 1];
10
+
11
+ if (inString) {
12
+ output += char;
13
+ if (escaped) {
14
+ escaped = false;
15
+ } else if (char === '\\') {
16
+ escaped = true;
17
+ } else if (char === '"') {
18
+ inString = false;
19
+ }
20
+ continue;
21
+ }
22
+
23
+ if (char === '"') {
24
+ inString = true;
25
+ output += char;
26
+ continue;
27
+ }
28
+
29
+ if (char === '/' && next === '/') {
30
+ while (index < input.length && input[index] !== '\n') {
31
+ index += 1;
32
+ }
33
+ if (index < input.length) {
34
+ output += '\n';
35
+ }
36
+ continue;
37
+ }
38
+
39
+ if (char === '/' && next === '*') {
40
+ index += 2;
41
+ while (index < input.length && !(input[index] === '*' && input[index + 1] === '/')) {
42
+ if (input[index] === '\n') {
43
+ output += '\n';
44
+ }
45
+ index += 1;
46
+ }
47
+ index += 1;
48
+ continue;
49
+ }
50
+
51
+ output += char;
52
+ }
53
+
54
+ return output;
5
55
  }
6
56
 
7
57
  function parseJsonWithComments(text) {
@@ -2,7 +2,7 @@ const fs = require('fs');
2
2
  const path = require('path');
3
3
  const { catFileExists, currentBranch, hasLocalBranch, hasRemoteBranch, mergeBaseIsAncestor, revListCount, runGit, statusPorcelain, worktreeList } = require('./git');
4
4
  const { parseJsonWithComments } = require('./json');
5
- const { buildGraph, readAllSlices, SliceGraphError, topoSort } = require('./slice-graph');
5
+ const { buildGraph, normalizeDeclaredDependencies, readAllSlices, SliceGraphError, topoSort } = require('./slice-graph');
6
6
  const { resolveSliceContext, toAlias } = require('./slice');
7
7
 
8
8
  function ensureExists(filePath, message) {
@@ -117,6 +117,17 @@ function baseRecoveryMessage(remote, baseBranch) {
117
117
  return `No se encontro la base '${baseBranch}' como rama local ni como '${remote}/${baseBranch}'. Para validacion estructural usa --local; para validacion contra otra base usa --base <branch>; o configura/fetchea el remoto '${remote}'.`;
118
118
  }
119
119
 
120
+ function resolveReadinessRoot(localMode) {
121
+ try {
122
+ return runGit(['rev-parse', '--show-toplevel'], process.cwd());
123
+ } catch (error) {
124
+ if (localMode) {
125
+ return process.cwd();
126
+ }
127
+ throw error;
128
+ }
129
+ }
130
+
120
131
  function validateSliceDocumentedOnBase(repoRoot, slice, options = {}) {
121
132
  const gate = options.gate || 'execution';
122
133
  const remote = options.remote || 'origin';
@@ -178,8 +189,12 @@ function validateDeclaredDependencyContract(repoRoot, slice) {
178
189
  throw new Error(`create-quiver: depends_on contiene referencias duplicadas en ${currentRef}.`);
179
190
  }
180
191
 
192
+ const normalizedDeclared = normalizeDeclaredDependencies(currentNode, declared);
193
+ if (normalizedDeclared.length !== new Set(normalizedDeclared).size) {
194
+ throw new Error(`create-quiver: depends_on contiene referencias duplicadas en ${currentRef}.`);
195
+ }
181
196
  const currentSet = new Set(currentNode.depends_on || []);
182
- for (const dep of declared) {
197
+ for (const dep of normalizedDeclared) {
183
198
  if (!currentSet.has(dep)) {
184
199
  throw new Error(`create-quiver: depends_on apunta a una referencia inexistente o invalida: ${dep}`);
185
200
  }
@@ -209,7 +224,7 @@ function checkSliceReadiness(sliceInput, options = {}) {
209
224
  const localMode = options.local === true;
210
225
  const strictOverlap = options.strictOverlap === true;
211
226
  const remote = options.remote || 'origin';
212
- const repoRoot = runGit(['rev-parse', '--show-toplevel'], process.cwd());
227
+ const repoRoot = resolveReadinessRoot(localMode);
213
228
  const slice = resolveSliceContext(repoRoot, sliceInput);
214
229
  const baseBranch = options.baseBranch || slice.baseBranch || 'develop';
215
230
 
@@ -19,6 +19,48 @@ function normalizeScopePath(filePath) {
19
19
  return normalizeContextPath(filePath);
20
20
  }
21
21
 
22
+ function globToRegExp(pattern) {
23
+ const source = String(pattern || '')
24
+ .split('')
25
+ .map((char, index, chars) => {
26
+ if (char === '*') {
27
+ return chars[index + 1] === '*' ? '\0' : '[^/]*';
28
+ }
29
+ if (char === '\0') {
30
+ return '.*';
31
+ }
32
+ return /[\\^$+?.()|[\]{}]/.test(char) ? `\\${char}` : char;
33
+ })
34
+ .join('')
35
+ .replace(/\0\[\^\/\]\*/g, '.*');
36
+
37
+ return new RegExp(`^${source}$`);
38
+ }
39
+
40
+ function allowedPathMatches(filePath, allowedPath) {
41
+ const file = normalizeScopePath(filePath);
42
+ const allowed = normalizeScopePath(allowedPath);
43
+
44
+ if (!file || !allowed) {
45
+ return false;
46
+ }
47
+
48
+ if (file === allowed) {
49
+ return true;
50
+ }
51
+
52
+ if (allowed.endsWith('/**')) {
53
+ const prefix = allowed.slice(0, -3);
54
+ return file === prefix || file.startsWith(`${prefix}/`);
55
+ }
56
+
57
+ if (allowed.includes('*')) {
58
+ return globToRegExp(allowed).test(file);
59
+ }
60
+
61
+ return false;
62
+ }
63
+
22
64
  function parseStatusPorcelain(text) {
23
65
  if (!text) {
24
66
  return [];
@@ -74,30 +116,30 @@ function diffWorktreeSnapshots(beforeSnapshot, afterSnapshot) {
74
116
  }
75
117
 
76
118
  function validateScopeSnapshot({ allowedFiles = [], beforeSnapshot, afterSnapshot, strict = true } = {}) {
77
- const normalizedAllowedFiles = new Set(
119
+ const normalizedAllowedFiles = Array.from(new Set(
78
120
  Array.isArray(allowedFiles)
79
121
  ? allowedFiles.map(normalizeScopePath).filter(Boolean)
80
122
  : [],
81
- );
123
+ ));
82
124
  const changedFiles = diffWorktreeSnapshots(beforeSnapshot, afterSnapshot);
83
- const outOfScopeFiles = changedFiles.filter((file) => !normalizedAllowedFiles.has(file));
125
+ const outOfScopeFiles = changedFiles.filter((file) => !normalizedAllowedFiles.some((allowedFile) => allowedPathMatches(file, allowedFile)));
84
126
 
85
127
  if (outOfScopeFiles.length === 0) {
86
128
  return {
87
129
  ok: true,
88
130
  changedFiles,
89
131
  outOfScopeFiles,
90
- allowedFiles: Array.from(normalizedAllowedFiles),
132
+ allowedFiles: normalizedAllowedFiles,
91
133
  beforeSnapshot,
92
134
  afterSnapshot,
93
135
  };
94
136
  }
95
137
 
96
138
  const message = formatError(
97
- `scope violation detected: changed files outside slice.json.files: ${outOfScopeFiles.join(', ')}`,
139
+ `scope violation detected: changed files outside declared slice scope: ${outOfScopeFiles.join(', ')}`,
98
140
  );
99
141
  const error = new ScopeValidationError('SCOPE_VIOLATION', message, {
100
- allowedFiles: Array.from(normalizedAllowedFiles),
142
+ allowedFiles: normalizedAllowedFiles,
101
143
  beforeSnapshot,
102
144
  afterSnapshot,
103
145
  changedFiles,
@@ -112,7 +154,7 @@ function validateScopeSnapshot({ allowedFiles = [], beforeSnapshot, afterSnapsho
112
154
  ok: false,
113
155
  changedFiles,
114
156
  outOfScopeFiles,
115
- allowedFiles: Array.from(normalizedAllowedFiles),
157
+ allowedFiles: normalizedAllowedFiles,
116
158
  beforeSnapshot,
117
159
  afterSnapshot,
118
160
  error,
@@ -121,6 +163,7 @@ function validateScopeSnapshot({ allowedFiles = [], beforeSnapshot, afterSnapsho
121
163
 
122
164
  module.exports = {
123
165
  ScopeValidationError,
166
+ allowedPathMatches,
124
167
  captureWorktreeSnapshot,
125
168
  diffWorktreeSnapshots,
126
169
  checkScope,
@@ -66,6 +66,15 @@ function sortFileList(files) {
66
66
  return Array.from(new Set((Array.isArray(files) ? files : []).map((file) => String(file)).filter(Boolean))).sort((a, b) => a.localeCompare(b));
67
67
  }
68
68
 
69
+ function resolveWriteScope(json) {
70
+ const allowedWritePaths = sortFileList(json?.allowed_write_paths);
71
+ if (allowedWritePaths.length > 0) {
72
+ return allowedWritePaths;
73
+ }
74
+
75
+ return sortFileList(json?.files);
76
+ }
77
+
69
78
  function normalizeDependencyRef(slice, dependency) {
70
79
  const dep = String(dependency || '').trim();
71
80
  if (!dep) {
@@ -95,6 +104,61 @@ function normalizeDependencyRef(slice, dependency) {
95
104
  return `${slice.specSlug}/${dep}`;
96
105
  }
97
106
 
107
+ function readSliceFile(rootDir, rootName, specSlug, sliceDirName) {
108
+ const sliceDir = path.join(rootDir, rootName, specSlug, 'slices', sliceDirName);
109
+ const slicePath = path.join(sliceDir, 'slice.json');
110
+ if (!fs.existsSync(slicePath)) {
111
+ return null;
112
+ }
113
+
114
+ const json = parseJsonWithComments(fs.readFileSync(slicePath, 'utf8'));
115
+ const sliceId = String(json.slice_id || sliceDirName).trim();
116
+ const ref = `${specSlug}/${sliceId}`;
117
+ return {
118
+ ref,
119
+ specFamily: rootName,
120
+ specSlug,
121
+ sliceId,
122
+ slicePath,
123
+ sliceDir,
124
+ files: resolveWriteScope(json),
125
+ expected_read_paths: sortFileList(json.expected_read_paths),
126
+ allowed_write_paths: sortFileList(json.allowed_write_paths),
127
+ validation_hints: sortFileList(json.validation_hints),
128
+ dependencies: Array.isArray(json.dependencies) ? json.dependencies.map((item) => String(item).trim()).filter(Boolean) : [],
129
+ depends_on: Array.isArray(json.depends_on) ? json.depends_on.map((item) => String(item).trim()).filter(Boolean) : [],
130
+ parallel_safe: typeof json.parallel_safe === 'string' ? json.parallel_safe : null,
131
+ parallel_safe_reason: typeof json.parallel_safe_reason === 'string' ? json.parallel_safe_reason : null,
132
+ status: typeof json.status === 'string' ? json.status : 'draft',
133
+ ticket: typeof json.ticket === 'string' ? json.ticket : '',
134
+ title: typeof json.title === 'string' ? json.title : sliceId,
135
+ json,
136
+ };
137
+ }
138
+
139
+ function readSpecSlices(rootDir, rootName, specSlug) {
140
+ const slicesDir = path.join(rootDir, rootName, specSlug, 'slices');
141
+ if (!fs.existsSync(slicesDir)) {
142
+ return [];
143
+ }
144
+
145
+ const slices = [];
146
+
147
+ for (const sliceEntry of fs.readdirSync(slicesDir, { withFileTypes: true })) {
148
+ if (!sliceEntry.isDirectory() || isPlaceholderSliceDir(sliceEntry.name)) {
149
+ continue;
150
+ }
151
+
152
+ const slice = readSliceFile(rootDir, rootName, specSlug, sliceEntry.name);
153
+ if (slice) {
154
+ slices.push(slice);
155
+ }
156
+ }
157
+
158
+ slices.sort((left, right) => compareSliceRefs(left.ref, right.ref));
159
+ return slices;
160
+ }
161
+
98
162
  function readAllSlices(rootDir) {
99
163
  const roots = ['specs', 'specs-fix'];
100
164
  const slices = [];
@@ -110,44 +174,7 @@ function readAllSlices(rootDir) {
110
174
  continue;
111
175
  }
112
176
 
113
- const specSlug = specEntry.name;
114
- const slicesDir = path.join(rootPath, specSlug, 'slices');
115
- if (!fs.existsSync(slicesDir)) {
116
- continue;
117
- }
118
-
119
- for (const sliceEntry of fs.readdirSync(slicesDir, { withFileTypes: true })) {
120
- if (!sliceEntry.isDirectory() || isPlaceholderSliceDir(sliceEntry.name)) {
121
- continue;
122
- }
123
-
124
- const sliceDir = path.join(slicesDir, sliceEntry.name);
125
- const slicePath = path.join(sliceDir, 'slice.json');
126
- if (!fs.existsSync(slicePath)) {
127
- continue;
128
- }
129
-
130
- const json = parseJsonWithComments(fs.readFileSync(slicePath, 'utf8'));
131
- const sliceId = String(json.slice_id || sliceEntry.name).trim();
132
- const ref = `${specSlug}/${sliceId}`;
133
- slices.push({
134
- ref,
135
- specFamily: rootName,
136
- specSlug,
137
- sliceId,
138
- slicePath,
139
- sliceDir,
140
- files: sortFileList(json.files),
141
- dependencies: Array.isArray(json.dependencies) ? json.dependencies.map((item) => String(item).trim()).filter(Boolean) : [],
142
- depends_on: Array.isArray(json.depends_on) ? json.depends_on.map((item) => String(item).trim()).filter(Boolean) : [],
143
- parallel_safe: typeof json.parallel_safe === 'string' ? json.parallel_safe : null,
144
- parallel_safe_reason: typeof json.parallel_safe_reason === 'string' ? json.parallel_safe_reason : null,
145
- status: typeof json.status === 'string' ? json.status : 'draft',
146
- ticket: typeof json.ticket === 'string' ? json.ticket : '',
147
- title: typeof json.title === 'string' ? json.title : sliceId,
148
- json,
149
- });
150
- }
177
+ slices.push(...readSpecSlices(rootDir, rootName, specEntry.name));
151
178
  }
152
179
  }
153
180
 
@@ -171,6 +198,75 @@ function declaredDependenciesForSlice(slice) {
171
198
  return null;
172
199
  }
173
200
 
201
+ function readSliceByRef(rootDir, ref) {
202
+ const value = String(ref || '').trim();
203
+ const slashIndex = value.indexOf('/');
204
+ if (slashIndex === -1) {
205
+ return null;
206
+ }
207
+
208
+ const specSlug = value.slice(0, slashIndex);
209
+ const sliceId = value.slice(slashIndex + 1);
210
+ if (!specSlug || !sliceId) {
211
+ return null;
212
+ }
213
+
214
+ for (const rootName of ['specs', 'specs-fix']) {
215
+ const direct = readSliceFile(rootDir, rootName, specSlug, sliceId);
216
+ if (direct) {
217
+ return direct;
218
+ }
219
+
220
+ for (const slice of readSpecSlices(rootDir, rootName, specSlug)) {
221
+ if (slice.ref === value) {
222
+ return slice;
223
+ }
224
+ }
225
+ }
226
+
227
+ return null;
228
+ }
229
+
230
+ function readSlicesForSpec(rootDir, specSlug) {
231
+ const targetSpec = String(specSlug || '').trim();
232
+ if (!targetSpec) {
233
+ return readAllSlices(rootDir);
234
+ }
235
+
236
+ const byRef = new Map();
237
+ const queue = [];
238
+
239
+ function addSlice(slice) {
240
+ if (!slice || byRef.has(slice.ref)) {
241
+ return;
242
+ }
243
+ byRef.set(slice.ref, slice);
244
+ queue.push(slice);
245
+ }
246
+
247
+ for (const rootName of ['specs', 'specs-fix']) {
248
+ for (const slice of readSpecSlices(rootDir, rootName, targetSpec)) {
249
+ addSlice(slice);
250
+ }
251
+ }
252
+
253
+ while (queue.length > 0) {
254
+ const slice = queue.shift();
255
+ for (const dep of declaredDependenciesForSlice(slice) || []) {
256
+ if (byRef.has(dep)) {
257
+ continue;
258
+ }
259
+
260
+ const dependencySlice = readSliceByRef(rootDir, dep);
261
+ if (dependencySlice) {
262
+ addSlice(dependencySlice);
263
+ }
264
+ }
265
+ }
266
+
267
+ return Array.from(byRef.values()).sort((left, right) => compareSliceRefs(left.ref, right.ref));
268
+ }
269
+
174
270
  function inferDependencies(slices) {
175
271
  const bySpec = new Map();
176
272
  const normalized = slices.map((slice) => ({
@@ -454,7 +550,11 @@ module.exports = {
454
550
  detectFileConflicts,
455
551
  inferDependencies,
456
552
  isFoundationSliceId,
553
+ normalizeDeclaredDependencies,
554
+ normalizeDependencyRef,
457
555
  readAllSlices,
556
+ readSlicesForSpec,
458
557
  naturalNumberFromSliceId,
558
+ resolveWriteScope,
459
559
  topoSort,
460
560
  };
@@ -42,14 +42,18 @@ function readSliceMeta(slicePath) {
42
42
  const branchName = typeof git.branch_name === 'string' ? git.branch_name.trim() : '';
43
43
  const sliceId = typeof json.slice_id === 'string' ? json.slice_id.trim() : '';
44
44
  const status = String(json.status || 'draft').trim() || 'draft';
45
+ const allowedWritePaths = Array.isArray(json.allowed_write_paths) ? json.allowed_write_paths.map((item) => String(item)) : [];
46
+ const legacyFiles = Array.isArray(json.files) ? json.files.map((item) => String(item)) : [];
45
47
 
46
48
  return {
47
49
  acceptance: Array.isArray(json.acceptance) ? json.acceptance : [],
50
+ allowedWritePaths,
48
51
  baseBranch,
49
52
  branchName,
50
53
  branchSlug,
51
54
  branchType,
52
- files: Array.isArray(json.files) ? json.files : [],
55
+ expectedReadPaths: Array.isArray(json.expected_read_paths) ? json.expected_read_paths.map((item) => String(item)) : [],
56
+ files: allowedWritePaths.length > 0 ? allowedWritePaths : legacyFiles,
53
57
  git,
54
58
  isBaseline: sliceId.startsWith('slice-00'),
55
59
  json,
@@ -60,6 +64,7 @@ function readSliceMeta(slicePath) {
60
64
  status,
61
65
  tests: Array.isArray(json.tests) ? json.tests : [],
62
66
  ticket,
67
+ validationHints: Array.isArray(json.validation_hints) ? json.validation_hints.map((item) => String(item)) : [],
63
68
  };
64
69
  }
65
70
 
@@ -185,6 +185,7 @@ function startSpecWorktree(repoRoot, specInput, options = {}) {
185
185
  return {
186
186
  ...identity,
187
187
  baseRef,
188
+ dryRun: options.dryRun === true,
188
189
  reused: true,
189
190
  slice00,
190
191
  worktreePath: existingWorktree,
@@ -199,6 +200,17 @@ function startSpecWorktree(repoRoot, specInput, options = {}) {
199
200
  throw new Error(formatError('current checkout is not clean. Commit or stash before starting a spec worktree.'));
200
201
  }
201
202
 
203
+ if (options.dryRun === true) {
204
+ return {
205
+ ...identity,
206
+ baseRef,
207
+ currentBranch: currentBranch(repoRoot),
208
+ dryRun: true,
209
+ reused: false,
210
+ slice00,
211
+ };
212
+ }
213
+
202
214
  worktreePrune(repoRoot);
203
215
  fs.mkdirSync(path.dirname(identity.worktreePath), { recursive: true });
204
216
 
@@ -212,6 +224,7 @@ function startSpecWorktree(repoRoot, specInput, options = {}) {
212
224
  ...identity,
213
225
  baseRef,
214
226
  currentBranch: currentBranch(repoRoot),
227
+ dryRun: false,
215
228
  reused: false,
216
229
  slice00,
217
230
  };
@@ -219,13 +232,14 @@ function startSpecWorktree(repoRoot, specInput, options = {}) {
219
232
 
220
233
  function formatSpecStartResult(result) {
221
234
  return `${[
222
- 'Spec worktree ready',
235
+ result.dryRun ? 'Spec worktree start dry-run' : 'Spec worktree ready',
223
236
  `Branch: ${result.branchName}`,
224
237
  `Base: ${result.baseRef}`,
225
238
  `Worktree: ${result.worktreePath}`,
226
239
  `Reused: ${result.reused ? 'yes' : 'no'}`,
227
240
  `slice-00: ${result.slice00 ? result.slice00.status : 'missing'}`,
228
- ].join('\n')}\n`;
241
+ result.dryRun && !result.reused ? `Would create worktree: ${result.worktreePath}` : '',
242
+ ].filter(Boolean).join('\n')}\n`;
229
243
  }
230
244
 
231
245
  function closeSpecWorktree(repoRoot, specInput, options = {}) {