create-quiver 0.12.0 → 0.13.0

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 (158) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/README.md +65 -25
  3. package/README_FOR_AI.md +36 -29
  4. package/ROADMAP.md +22 -3
  5. package/docs/AI_ONBOARDING_PROMPT.md.template +7 -1
  6. package/docs/COMMANDS.md.template +53 -20
  7. package/docs/STATUS.md.template +5 -1
  8. package/docs/WORKFLOW.md.template +13 -11
  9. package/package.json +10 -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/specs/quiver-v27-reliability-ai-workflow-hardening/AUDIT_V24_V25_V26.md +67 -0
  81. package/specs/quiver-v27-reliability-ai-workflow-hardening/COMMAND_CONTRACTS.md +125 -0
  82. package/specs/quiver-v27-reliability-ai-workflow-hardening/COVERAGE_MATRIX.md +74 -0
  83. package/specs/quiver-v27-reliability-ai-workflow-hardening/EVIDENCE_REPORT.md +179 -0
  84. package/specs/quiver-v27-reliability-ai-workflow-hardening/EXECUTION_PLAN.md +71 -0
  85. package/specs/quiver-v27-reliability-ai-workflow-hardening/SPEC.md +176 -0
  86. package/specs/quiver-v27-reliability-ai-workflow-hardening/STATUS.md +37 -0
  87. package/specs/quiver-v27-reliability-ai-workflow-hardening/pr.md +132 -0
  88. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-00-docs-audit-coverage-and-contracts/CLOSURE_BRIEF.md +36 -0
  89. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-00-docs-audit-coverage-and-contracts/EXECUTION_BRIEF.md +56 -0
  90. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-00-docs-audit-coverage-and-contracts/slice.json +75 -0
  91. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-01-core-state-resolver-and-canonical-statuses/CLOSURE_BRIEF.md +37 -0
  92. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-01-core-state-resolver-and-canonical-statuses/EXECUTION_BRIEF.md +54 -0
  93. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-01-core-state-resolver-and-canonical-statuses/slice.json +79 -0
  94. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-02-json-export-contract-and-machine-output/CLOSURE_BRIEF.md +34 -0
  95. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-02-json-export-contract-and-machine-output/EXECUTION_BRIEF.md +54 -0
  96. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-02-json-export-contract-and-machine-output/slice.json +75 -0
  97. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-03-approved-plan-to-spec-create/CLOSURE_BRIEF.md +36 -0
  98. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-03-approved-plan-to-spec-create/EXECUTION_BRIEF.md +55 -0
  99. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-03-approved-plan-to-spec-create/slice.json +78 -0
  100. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-04-ai-artifact-storage-redaction-and-token-compaction/CLOSURE_BRIEF.md +31 -0
  101. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-04-ai-artifact-storage-redaction-and-token-compaction/EXECUTION_BRIEF.md +55 -0
  102. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-04-ai-artifact-storage-redaction-and-token-compaction/slice.json +77 -0
  103. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-05-worktree-lifecycle-locks-and-recovery/CLOSURE_BRIEF.md +31 -0
  104. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-05-worktree-lifecycle-locks-and-recovery/EXECUTION_BRIEF.md +55 -0
  105. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-05-worktree-lifecycle-locks-and-recovery/slice.json +84 -0
  106. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-06-validation-gates-and-scope-safety/CLOSURE_BRIEF.md +32 -0
  107. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-06-validation-gates-and-scope-safety/EXECUTION_BRIEF.md +57 -0
  108. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-06-validation-gates-and-scope-safety/slice.json +99 -0
  109. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-07-context-analysis-and-doctor-flow/CLOSURE_BRIEF.md +31 -0
  110. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-07-context-analysis-and-doctor-flow/EXECUTION_BRIEF.md +57 -0
  111. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-07-context-analysis-and-doctor-flow/slice.json +88 -0
  112. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-08-cross-platform-help-auth-and-dx/CLOSURE_BRIEF.md +31 -0
  113. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-08-cross-platform-help-auth-and-dx/EXECUTION_BRIEF.md +56 -0
  114. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-08-cross-platform-help-auth-and-dx/slice.json +85 -0
  115. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-09-fixtures-smoke-docs-and-release-readiness/CLOSURE_BRIEF.md +32 -0
  116. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-09-fixtures-smoke-docs-and-release-readiness/EXECUTION_BRIEF.md +56 -0
  117. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-09-fixtures-smoke-docs-and-release-readiness/slice.json +91 -0
  118. package/src/create-quiver/commands/ai.js +652 -27
  119. package/src/create-quiver/commands/flow.js +58 -9
  120. package/src/create-quiver/commands/graph.js +11 -9
  121. package/src/create-quiver/commands/plan.js +7 -16
  122. package/src/create-quiver/commands/spec.js +282 -0
  123. package/src/create-quiver/index.js +409 -31
  124. package/src/create-quiver/lib/actionable-error.js +27 -0
  125. package/src/create-quiver/lib/agent-profiles.js +16 -4
  126. package/src/create-quiver/lib/ai/artifacts.js +318 -0
  127. package/src/create-quiver/lib/ai/context-packs.js +4 -0
  128. package/src/create-quiver/lib/ai/execution-plan.js +16 -1
  129. package/src/create-quiver/lib/ai/executor.js +272 -21
  130. package/src/create-quiver/lib/ai/export-state.js +679 -0
  131. package/src/create-quiver/lib/ai/github.js +162 -2
  132. package/src/create-quiver/lib/ai/onboarding-template.js +215 -2
  133. package/src/create-quiver/lib/ai/plan-review.js +7 -2
  134. package/src/create-quiver/lib/ai/providers.js +4 -3
  135. package/src/create-quiver/lib/ai/run-state.js +414 -0
  136. package/src/create-quiver/lib/ai/spec-generator.js +84 -13
  137. package/src/create-quiver/lib/ai/spec-templates.js +150 -21
  138. package/src/create-quiver/lib/analyze.js +2 -2
  139. package/src/create-quiver/lib/approvals.js +36 -5
  140. package/src/create-quiver/lib/demo.js +189 -14
  141. package/src/create-quiver/lib/doctor.js +154 -0
  142. package/src/create-quiver/lib/git.js +40 -1
  143. package/src/create-quiver/lib/handoff.js +123 -12
  144. package/src/create-quiver/lib/init-docs.js +35 -13
  145. package/src/create-quiver/lib/init-layout.js +9 -0
  146. package/src/create-quiver/lib/json.js +53 -3
  147. package/src/create-quiver/lib/lifecycle.js +52 -3
  148. package/src/create-quiver/lib/locks.js +134 -0
  149. package/src/create-quiver/lib/package-safety.js +7 -0
  150. package/src/create-quiver/lib/paths.js +74 -0
  151. package/src/create-quiver/lib/project-scan.js +74 -0
  152. package/src/create-quiver/lib/project-state-resolver.js +236 -0
  153. package/src/create-quiver/lib/readiness.js +66 -10
  154. package/src/create-quiver/lib/scope.js +52 -8
  155. package/src/create-quiver/lib/slice-graph.js +138 -38
  156. package/src/create-quiver/lib/slice.js +14 -5
  157. package/src/create-quiver/lib/spec-worktrees.js +129 -32
  158. package/src/create-quiver/lib/statuses.js +115 -0
@@ -2,8 +2,9 @@ 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');
6
- const { resolveSliceContext, toAlias } = require('./slice');
5
+ const { buildGraph, normalizeDeclaredDependencies, readAllSlices, SliceGraphError, topoSort } = require('./slice-graph');
6
+ const { resolveSliceContext, toAlias, validateSliceMetaForStart } = require('./slice');
7
+ const { validateProjectRelativePaths } = require('./paths');
7
8
 
8
9
  function ensureExists(filePath, message) {
9
10
  if (!fs.existsSync(filePath)) {
@@ -111,12 +112,30 @@ function validateLocalSliceArtifacts(repoRoot, slice) {
111
112
  throw new Error('create-quiver: slice.json.files contiene entradas invalidas.');
112
113
  }
113
114
  console.log('PASS: slice.json declara archivos de alcance.');
115
+
116
+ validateSliceMetaForStart(slice);
117
+ console.log('PASS: slice.json declara metadata git compatible con start-slice.');
118
+
119
+ validateProjectRelativePaths(slice.files, 'slice.json files/allowed_write_paths');
120
+ validateProjectRelativePaths(slice.expectedReadPaths, 'slice.json expected_read_paths');
121
+ console.log('PASS: slice.json declara rutas relativas seguras dentro del proyecto.');
114
122
  }
115
123
 
116
124
  function baseRecoveryMessage(remote, baseBranch) {
117
125
  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
126
  }
119
127
 
128
+ function resolveReadinessRoot(localMode) {
129
+ try {
130
+ return runGit(['rev-parse', '--show-toplevel'], process.cwd());
131
+ } catch (error) {
132
+ if (localMode) {
133
+ return process.cwd();
134
+ }
135
+ throw error;
136
+ }
137
+ }
138
+
120
139
  function validateSliceDocumentedOnBase(repoRoot, slice, options = {}) {
121
140
  const gate = options.gate || 'execution';
122
141
  const remote = options.remote || 'origin';
@@ -178,8 +197,12 @@ function validateDeclaredDependencyContract(repoRoot, slice) {
178
197
  throw new Error(`create-quiver: depends_on contiene referencias duplicadas en ${currentRef}.`);
179
198
  }
180
199
 
200
+ const normalizedDeclared = normalizeDeclaredDependencies(currentNode, declared);
201
+ if (normalizedDeclared.length !== new Set(normalizedDeclared).size) {
202
+ throw new Error(`create-quiver: depends_on contiene referencias duplicadas en ${currentRef}.`);
203
+ }
181
204
  const currentSet = new Set(currentNode.depends_on || []);
182
- for (const dep of declared) {
205
+ for (const dep of normalizedDeclared) {
183
206
  if (!currentSet.has(dep)) {
184
207
  throw new Error(`create-quiver: depends_on apunta a una referencia inexistente o invalida: ${dep}`);
185
208
  }
@@ -204,12 +227,17 @@ function validateDeclaredDependencyContract(repoRoot, slice) {
204
227
  }
205
228
  }
206
229
 
230
+ function localCheckSummary() {
231
+ console.log('INFO: Modo local: checks ejecutados: spec docs, briefs, metadata git, scope declarado, rutas seguras, dependencias y gate.');
232
+ console.log('INFO: Modo local: checks omitidos: existencia en base remota/local y overlap contra worktrees activos.');
233
+ }
234
+
207
235
  function checkSliceReadiness(sliceInput, options = {}) {
208
236
  const gate = options.gate || 'execution';
209
237
  const localMode = options.local === true;
210
238
  const strictOverlap = options.strictOverlap === true;
211
239
  const remote = options.remote || 'origin';
212
- const repoRoot = runGit(['rev-parse', '--show-toplevel'], process.cwd());
240
+ const repoRoot = resolveReadinessRoot(localMode);
213
241
  const slice = resolveSliceContext(repoRoot, sliceInput);
214
242
  const baseBranch = options.baseBranch || slice.baseBranch || 'develop';
215
243
 
@@ -247,6 +275,9 @@ function checkSliceReadiness(sliceInput, options = {}) {
247
275
  }
248
276
 
249
277
  validateDeclaredDependencyContract(repoRoot, slice);
278
+ if (localMode) {
279
+ localCheckSummary();
280
+ }
250
281
 
251
282
  switch (gate) {
252
283
  case 'ready':
@@ -356,22 +387,47 @@ function checkPrReadiness(sliceInput) {
356
387
 
357
388
  function checkScope(sliceInput, options = {}) {
358
389
  const strict = options.strict === true;
390
+ const remote = options.remote || 'origin';
359
391
  const repoRoot = runGit(['rev-parse', '--show-toplevel'], process.cwd());
360
392
  const slice = resolveSliceContext(repoRoot, sliceInput);
361
393
  const declared = slice.files;
394
+ validateProjectRelativePaths(declared, 'slice scope path');
395
+
396
+ const explicitBaseBranch = typeof options.baseBranch === 'string' ? options.baseBranch.trim() : '';
397
+ const candidateBaseBranches = Array.from(new Set([
398
+ explicitBaseBranch,
399
+ slice.baseBranch,
400
+ 'main',
401
+ 'develop',
402
+ 'master',
403
+ ].filter(Boolean)));
404
+
405
+ let baseRef = '';
406
+ let baseSource = '';
407
+ for (const candidate of candidateBaseBranches) {
408
+ if (hasRemoteBranch(repoRoot, candidate, remote)) {
409
+ baseRef = `${remote}/${candidate}`;
410
+ baseSource = explicitBaseBranch === candidate ? '--base' : candidate === slice.baseBranch ? 'slice.git.base_branch' : 'fallback';
411
+ break;
412
+ }
413
+ if (hasLocalBranch(repoRoot, candidate)) {
414
+ baseRef = candidate;
415
+ baseSource = explicitBaseBranch === candidate ? '--base' : candidate === slice.baseBranch ? 'slice.git.base_branch' : 'fallback';
416
+ break;
417
+ }
418
+ }
362
419
 
363
420
  let touchedRaw = '';
364
- if (hasRemoteBranch(repoRoot, 'develop')) {
365
- touchedRaw = runGit(['diff', '--name-only', 'origin/develop...HEAD'], repoRoot);
366
- } else if (hasLocalBranch(repoRoot, 'develop')) {
367
- touchedRaw = runGit(['diff', '--name-only', 'develop...HEAD'], repoRoot);
421
+ if (baseRef) {
422
+ touchedRaw = runGit(['diff', '--name-only', `${baseRef}...HEAD`], repoRoot);
423
+ console.log(`INFO: check-scope base: ${baseRef} (${baseSource}).`);
368
424
  } else {
369
- console.log('WARN: No se encontro rama origin/develop ni develop. Saltando check de scope.');
425
+ console.log(`WARN: No se encontro base para check-scope. Probadas: ${candidateBaseBranches.join(', ')}. Usa --base <branch> o configura git.base_branch en slice.json.`);
370
426
  return;
371
427
  }
372
428
 
373
429
  if (!touchedRaw) {
374
- console.log('WARN: No se encontraron archivos modificados respecto de develop.');
430
+ console.log(`WARN: No se encontraron archivos modificados respecto de ${baseRef}.`);
375
431
  return;
376
432
  }
377
433
 
@@ -1,6 +1,7 @@
1
1
  const { statusPorcelain } = require('./git');
2
2
  const { normalizeContextPath } = require('./ai/safety');
3
3
  const { checkScope } = require('./readiness');
4
+ const { validateProjectRelativePaths } = require('./paths');
4
5
 
5
6
  class ScopeValidationError extends Error {
6
7
  constructor(code, message, details = {}) {
@@ -19,6 +20,48 @@ function normalizeScopePath(filePath) {
19
20
  return normalizeContextPath(filePath);
20
21
  }
21
22
 
23
+ function globToRegExp(pattern) {
24
+ const source = String(pattern || '')
25
+ .split('')
26
+ .map((char, index, chars) => {
27
+ if (char === '*') {
28
+ return chars[index + 1] === '*' ? '\0' : '[^/]*';
29
+ }
30
+ if (char === '\0') {
31
+ return '.*';
32
+ }
33
+ return /[\\^$+?.()|[\]{}]/.test(char) ? `\\${char}` : char;
34
+ })
35
+ .join('')
36
+ .replace(/\0\[\^\/\]\*/g, '.*');
37
+
38
+ return new RegExp(`^${source}$`);
39
+ }
40
+
41
+ function allowedPathMatches(filePath, allowedPath) {
42
+ const file = normalizeScopePath(filePath);
43
+ const allowed = normalizeScopePath(allowedPath);
44
+
45
+ if (!file || !allowed) {
46
+ return false;
47
+ }
48
+
49
+ if (file === allowed) {
50
+ return true;
51
+ }
52
+
53
+ if (allowed.endsWith('/**')) {
54
+ const prefix = allowed.slice(0, -3);
55
+ return file === prefix || file.startsWith(`${prefix}/`);
56
+ }
57
+
58
+ if (allowed.includes('*')) {
59
+ return globToRegExp(allowed).test(file);
60
+ }
61
+
62
+ return false;
63
+ }
64
+
22
65
  function parseStatusPorcelain(text) {
23
66
  if (!text) {
24
67
  return [];
@@ -74,30 +117,30 @@ function diffWorktreeSnapshots(beforeSnapshot, afterSnapshot) {
74
117
  }
75
118
 
76
119
  function validateScopeSnapshot({ allowedFiles = [], beforeSnapshot, afterSnapshot, strict = true } = {}) {
77
- const normalizedAllowedFiles = new Set(
120
+ const normalizedAllowedFiles = Array.from(new Set(
78
121
  Array.isArray(allowedFiles)
79
- ? allowedFiles.map(normalizeScopePath).filter(Boolean)
122
+ ? validateProjectRelativePaths(allowedFiles, 'allowed scope path').map(normalizeScopePath).filter(Boolean)
80
123
  : [],
81
- );
124
+ ));
82
125
  const changedFiles = diffWorktreeSnapshots(beforeSnapshot, afterSnapshot);
83
- const outOfScopeFiles = changedFiles.filter((file) => !normalizedAllowedFiles.has(file));
126
+ const outOfScopeFiles = changedFiles.filter((file) => !normalizedAllowedFiles.some((allowedFile) => allowedPathMatches(file, allowedFile)));
84
127
 
85
128
  if (outOfScopeFiles.length === 0) {
86
129
  return {
87
130
  ok: true,
88
131
  changedFiles,
89
132
  outOfScopeFiles,
90
- allowedFiles: Array.from(normalizedAllowedFiles),
133
+ allowedFiles: normalizedAllowedFiles,
91
134
  beforeSnapshot,
92
135
  afterSnapshot,
93
136
  };
94
137
  }
95
138
 
96
139
  const message = formatError(
97
- `scope violation detected: changed files outside slice.json.files: ${outOfScopeFiles.join(', ')}`,
140
+ `scope violation detected: changed files outside declared slice scope: ${outOfScopeFiles.join(', ')}`,
98
141
  );
99
142
  const error = new ScopeValidationError('SCOPE_VIOLATION', message, {
100
- allowedFiles: Array.from(normalizedAllowedFiles),
143
+ allowedFiles: normalizedAllowedFiles,
101
144
  beforeSnapshot,
102
145
  afterSnapshot,
103
146
  changedFiles,
@@ -112,7 +155,7 @@ function validateScopeSnapshot({ allowedFiles = [], beforeSnapshot, afterSnapsho
112
155
  ok: false,
113
156
  changedFiles,
114
157
  outOfScopeFiles,
115
- allowedFiles: Array.from(normalizedAllowedFiles),
158
+ allowedFiles: normalizedAllowedFiles,
116
159
  beforeSnapshot,
117
160
  afterSnapshot,
118
161
  error,
@@ -121,6 +164,7 @@ function validateScopeSnapshot({ allowedFiles = [], beforeSnapshot, afterSnapsho
121
164
 
122
165
  module.exports = {
123
166
  ScopeValidationError,
167
+ allowedPathMatches,
124
168
  captureWorktreeSnapshot,
125
169
  diffWorktreeSnapshots,
126
170
  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
  };
@@ -1,7 +1,7 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
3
  const { parseJsonWithComments } = require('./json');
4
- const { normalizeGitBashDrivePath, relativePosixPath, resolveTargetRoot, specRelativePathFromPath, toPosixPath } = require('./paths');
4
+ const { assertPathInsideRoot, normalizeGitBashDrivePath, relativePosixPath, resolveTargetRoot, specRelativePathFromPath, toPosixPath } = require('./paths');
5
5
 
6
6
  function readJson(filePath) {
7
7
  return JSON.parse(fs.readFileSync(filePath, 'utf8'));
@@ -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
 
@@ -84,9 +89,12 @@ function validateSliceMetaForStart(slice) {
84
89
  throw new Error(`create-quiver: git.branch_type invalido: "${slice.branchType}". Usa "feature", "bugfix" o "hotfix".`);
85
90
  }
86
91
 
87
- const allowedBaseBranches = allowedBaseByType[slice.branchType];
88
- if (!allowedBaseBranches.includes(slice.baseBranch)) {
89
- throw new Error(`create-quiver: git.base_branch invalido para ${slice.branchType}. Usa "${allowedBaseBranches.join('" o "')}".`);
92
+ if (!/^[A-Za-z0-9._/-]+$/.test(slice.baseBranch)
93
+ || slice.baseBranch.includes('..')
94
+ || slice.baseBranch.startsWith('/')
95
+ || slice.baseBranch.endsWith('/')
96
+ || slice.baseBranch.includes('\\')) {
97
+ throw new Error('create-quiver: git.base_branch invalido. Usa una rama base valida como "main", "develop", "master" o "release/2026".');
90
98
  }
91
99
 
92
100
  const expectedBranchName = `${slice.branchType}/${slice.ticket}-${slice.branchSlug}`;
@@ -111,6 +119,7 @@ function resolveRepoSlicePath(repoRoot, relSlicePath) {
111
119
  function resolveSliceContext(repoRoot, slicePath) {
112
120
  const canonicalRepoRoot = canonicalizePath(repoRoot);
113
121
  let absSlicePath = resolveSlicePath(slicePath);
122
+ assertPathInsideRoot(canonicalRepoRoot, absSlicePath, 'slice path');
114
123
  let relSlicePath = relativePosixPath(canonicalRepoRoot, absSlicePath);
115
124
  let parts = relSlicePath.split('/');
116
125