@namewta/speculo 2.0.1 → 2.0.2

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 (73) hide show
  1. package/README.md +40 -29
  2. package/dist/src/agent-skills.d.ts +43 -17
  3. package/dist/src/agent-skills.js +93 -26
  4. package/dist/src/agent-skills.js.map +1 -1
  5. package/dist/src/cli.js +43 -27
  6. package/dist/src/cli.js.map +1 -1
  7. package/dist/src/index.d.ts +20 -6
  8. package/dist/src/index.js +36 -11
  9. package/dist/src/index.js.map +1 -1
  10. package/dist/src/install-ui.d.ts +61 -0
  11. package/dist/src/install-ui.js +268 -0
  12. package/dist/src/install-ui.js.map +1 -0
  13. package/dist/src/interaction.d.ts +5 -0
  14. package/dist/src/interaction.js +10 -0
  15. package/dist/src/interaction.js.map +1 -0
  16. package/dist/src/transaction.d.ts +2 -0
  17. package/dist/src/transaction.js +16 -0
  18. package/dist/src/transaction.js.map +1 -1
  19. package/dist/src/workflows.js +2 -2
  20. package/dist/src/workflows.js.map +1 -1
  21. package/package.json +2 -2
  22. package/template/canonical/canonical-specdev-goal-plan.md +138 -78
  23. package/template/canonical/canonical-specdev-grill-with-docs.md +818 -184
  24. package/template/canonical/canonical-specdev-spec.md +819 -185
  25. package/template/canonical/canonical-specdev-tickets.md +690 -56
  26. package/template/canonical/canonical-specdev-wayfinder.md +818 -184
  27. package/template/references/coordination/coordination-contract.md +8 -8
  28. package/template/references/coordination/modes/cli.md +5 -5
  29. package/template/references/coordination/modes/herdr.md +16 -1
  30. package/template/references/coordination/result-and-recovery.md +2 -2
  31. package/template/references/coordination/task-brief.md +5 -5
  32. package/template/workflows/specdev/SKILL.md +2 -2
  33. package/template/workflows/specdev/common/rules/artifact-contract.md +2 -2
  34. package/template/workflows/specdev/common/rules/change-completion.md +4 -2
  35. package/template/workflows/specdev/common/rules/domain-runtime.md +2 -2
  36. package/template/workflows/specdev/common/rules/evidence-and-verification.md +2 -2
  37. package/template/workflows/specdev/common/rules/execution-policy.md +28 -0
  38. package/template/workflows/specdev/common/rules/operating-governance.md +2 -2
  39. package/template/workflows/specdev/common/rules/path-ownership.md +3 -3
  40. package/template/workflows/specdev/common/rules/workflow-routing.md +3 -3
  41. package/template/workflows/specdev/common/skills/code-review/SKILL.md +2 -0
  42. package/template/workflows/specdev/common/skills/dev-worktree/SKILL.md +11 -11
  43. package/template/workflows/specdev/common/skills/dev-worktree/references/create.md +20 -12
  44. package/template/workflows/specdev/common/skills/dev-worktree/references/finalize.md +12 -12
  45. package/template/workflows/specdev/common/tools/README.md +1 -1
  46. package/template/workflows/specdev/common/tools/change-artifacts.mjs +5 -11
  47. package/template/workflows/specdev/common/tools/plan-contract.mjs +11 -3
  48. package/template/workflows/specdev/common/tools/tests/engineering.test.mjs +2 -2
  49. package/template/workflows/specdev/common/tools/validate-specdev.mjs +131 -77
  50. package/template/workflows/specdev/manifest.json +4 -6
  51. package/template/workflows/specdev/specdev-c-code-review/SKILL.md +2 -2
  52. package/template/workflows/specdev/specdev-i-implement/SKILL.md +5 -5
  53. package/template/workflows/specdev/specdev-i-implement/assets/evidence-template.md +7 -6
  54. package/template/workflows/specdev/specdev-i-implement/references/execution-preflight.md +8 -4
  55. package/template/workflows/specdev/specdev-i-implement/references/implementation-procedure.md +17 -17
  56. package/template/workflows/specdev/specdev-i-init-setup/SKILL.md +1 -1
  57. package/template/workflows/specdev/specdev-l-learn/SKILL.md +35 -0
  58. package/template/workflows/specdev/specdev-l-learn/references/learning-change.md +23 -0
  59. package/template/workflows/specdev/specdev-l-learn/references/lesson-format.md +61 -0
  60. package/template/workflows/specdev/specdev-p-goal-plan/SKILL.md +2 -2
  61. package/template/workflows/specdev/specdev-p-goal-plan/assets/goal-plan-template.md +2 -0
  62. package/template/workflows/specdev/specdev-p-goal-plan/references/orchestration-protocol.md +1 -1
  63. package/template/workflows/specdev/specdev-p-goal-plan/references/planning-modes.md +2 -2
  64. package/template/workflows/specdev/specdev-p-goal-plan/references/single-change-plan.md +2 -2
  65. package/template/workflows/specdev/specdev-r-retro/SKILL.md +1 -1
  66. package/template/workflows/specdev/specdev-s-spec/SKILL.md +1 -1
  67. package/template/workflows/specdev/specdev-t-tickets/SKILL.md +1 -1
  68. package/template/workflows/specdev/specdev-t-tickets/assets/ticket-template.md +2 -2
  69. package/template/workflows/specdev/specdev-t-tickets/assets/tickets-map-template.md +1 -1
  70. package/template/workflows/specdev/specdev-t-tickets/references/planning-procedure.md +1 -1
  71. package/template/workflows/specdev/specdev-t-tickets/references/ticket-readiness.md +1 -1
  72. package/template/workflows/specdev/specdev-l-learn-change/SKILL.md +0 -33
  73. package/template/workflows/specdev/specdev-l-learn-change/references/lesson-format.md +0 -51
@@ -28,7 +28,7 @@ import {
28
28
  validateGoalMap, validateInitiative, resolveProjectSource, readChangeExecution, readSpecification, resolveToolContext, validateExecutionPolicy, canonicalToolPath, resolveSnapshotContext, readLightweightGoal, validateAcceptanceEvidence,
29
29
  } from "./plan-contract.mjs";
30
30
 
31
- import { validateChangeClosure } from "./change-artifacts.mjs";
31
+ import { validateChangeClosure, readChangeOwner } from "./change-artifacts.mjs";
32
32
  import { validateRecord, validateRecords } from "./delivery-artifacts.mjs";
33
33
  import { reviewScopeCLI } from "./review-scope.mjs";
34
34
  import { ENGINEERING_KINDS, validateEngineeringArtifacts, validateEngineeringPackage } from "./engineering-evidence.mjs";
@@ -55,7 +55,7 @@ const EXPECTED_WORKS = new Set([
55
55
  "specdev-g-grill-with-docs",
56
56
  "specdev-i-implement",
57
57
  "specdev-i-init-setup",
58
- "specdev-l-learn-change",
58
+ "specdev-l-learn",
59
59
  "specdev-p-goal-plan",
60
60
  "specdev-p-prototype",
61
61
  "specdev-r-review-architecture",
@@ -172,7 +172,7 @@ const VALID_STAGES = new Set([
172
172
  "tickets",
173
173
  "goal-plan",
174
174
  "implement",
175
- "learn-change",
175
+ "learn",
176
176
  "retro",
177
177
  "pr-delivery",
178
178
  "review",
@@ -874,10 +874,10 @@ function capabilityChecks(root) {
874
874
  ],
875
875
  ],
876
876
  [
877
- "learn-change",
877
+ "learn",
878
878
  [
879
- join(root, "specdev-l-learn-change", "SKILL.md"),
880
- ["开发完成后", "零专业背景", "$ARGUMENTS", "ASCII", "learning/index.md", "{number}_{topic}.md", "{roots.state}/learning/"],
879
+ join(root, "specdev-l-learn", "SKILL.md"),
880
+ ["需求参考", "零专业背景", "$ARGUMENTS", "ASCII", "learning-change.md", "learning/index.md", "{number}_{topic}.md", "{roots.state}/learning/"],
881
881
  ],
882
882
  ],
883
883
  [
@@ -1586,7 +1586,7 @@ function validateChangeLearning(change, required, errors) {
1586
1586
  const diagramName = /^\d{2,}_[\p{L}\p{N}_-]+\.md$/u;
1587
1587
 
1588
1588
  if (!existsSync(learningRoot)) {
1589
- if (required) errors.push("learn-change stage requires learning/index.md");
1589
+ if (required) errors.push("learn stage requires learning/index.md");
1590
1590
  return null;
1591
1591
  }
1592
1592
  if (lstatSync(learningRoot).isSymbolicLink() || !isDirectory(learningRoot)) {
@@ -1595,22 +1595,27 @@ function validateChangeLearning(change, required, errors) {
1595
1595
  }
1596
1596
 
1597
1597
  const diagramFiles = [];
1598
+ const diagramNumbers = new Set();
1598
1599
  for (const entry of readdirSync(learningRoot, { withFileTypes: true })) {
1599
1600
  if (entry.isSymbolicLink()) {
1600
1601
  errors.push(`learning/${entry.name}: learning artifacts must not be symlinks`);
1601
1602
  } else if (entry.isFile() && diagramName.test(entry.name)) {
1602
1603
  diagramFiles.push(entry.name);
1604
+ const number = Number(entry.name.split("_")[0]);
1605
+ if (diagramNumbers.has(number)) errors.push(`learning/${entry.name}: duplicate diagram number ${number}`);
1606
+ diagramNumbers.add(number);
1603
1607
  }
1604
1608
  }
1605
1609
  diagramFiles.sort();
1606
1610
 
1607
1611
  if (!isFile(indexPath)) {
1608
- errors.push("learn-change stage requires learning/index.md");
1612
+ errors.push("learn stage requires learning/index.md");
1609
1613
  return null;
1610
1614
  }
1611
1615
 
1612
1616
  const index = readText(indexPath);
1613
- if (!index.includes("# Change 学习图解索引")) {
1617
+ // Historical titles remain readable without rewriting existing learning records.
1618
+ if (!/^# (?:Change )?学习图解索引\s*$/m.test(index)) {
1614
1619
  errors.push("learning/index.md: missing index heading");
1615
1620
  }
1616
1621
  const entries = Array.from(
@@ -2223,8 +2228,10 @@ function validateChangeExecution(change, errors, context) {
2223
2228
  errors.push(`${basename(path)}: invalid worktree entry`);
2224
2229
  continue;
2225
2230
  }
2231
+ const direct = worktree.purpose === "direct-spec";
2232
+ const unit = direct ? "direct-spec" : worktree.ticket_id;
2226
2233
  const requiredWorktreeKeys = [
2227
- "ticket_id",
2234
+ ...(direct ? ["purpose", "change", "integration_attempt_limit"] : ["ticket_id"]),
2228
2235
  "owner",
2229
2236
  "implementation_owner",
2230
2237
  "integration_owner",
@@ -2245,14 +2252,24 @@ function validateChangeExecution(change, errors, context) {
2245
2252
  }
2246
2253
  const unexpectedWorktreeKeys = Object.keys(worktree).filter((key) => !requiredWorktreeKeys.includes(key));
2247
2254
  if (unexpectedWorktreeKeys.length) {
2248
- errors.push(`${basename(path)}: worktree ${worktree.ticket_id} has unexpected keys ${JSON.stringify(unexpectedWorktreeKeys.sort())}`);
2255
+ errors.push(`${basename(path)}: worktree ${unit} has unexpected keys ${JSON.stringify(unexpectedWorktreeKeys.sort())}`);
2249
2256
  }
2250
- if (!/^T-[0-9]{2,}$/.test(String(worktree.ticket_id))) {
2251
- errors.push(`${basename(path)}: invalid worktree ticket_id ${worktree.ticket_id}`);
2252
- } else if (seenWorktreeTickets.has(worktree.ticket_id)) {
2253
- errors.push(`${basename(path)}: duplicate worktree for ${worktree.ticket_id}`);
2254
- }
2255
- seenWorktreeTickets.add(worktree.ticket_id);
2257
+ if (direct) {
2258
+ if (worktree.change !== expectedChange) errors.push("Direct Spec execution belongs to another change");
2259
+ try {
2260
+ if (readChangeOwner(context, expectedChange).owner !== worktree.owner) errors.push("Direct Spec execution owner differs from original Lead");
2261
+ } catch (error) { errors.push(error.message); }
2262
+ if (!Number.isInteger(worktree.integration_attempt_limit) || worktree.integration_attempt_limit < 1) errors.push("Direct Spec requires a positive integration_attempt_limit");
2263
+ if (worktree.integration?.attempts > worktree.integration_attempt_limit) errors.push("Direct Spec integration attempts exceed integration_attempt_limit");
2264
+ if (worktree.integration_owner !== worktree.owner) errors.push("Direct Spec integration owner must be Lead");
2265
+ if (worktree.status === "cancelled" && data.outcome !== "cancelled") errors.push("cancelled Direct Spec execution requires the original whole-change cancellation");
2266
+ if (worktree.workspace_ref !== "current" && worktree.branch !== `speculo/${expectedChange}/direct-spec`) errors.push("Direct Spec source branch must match its change");
2267
+ } else if (!/^T-[0-9]{2,}$/.test(String(unit))) {
2268
+ errors.push(`${basename(path)}: invalid worktree ticket_id ${unit}`);
2269
+ } else if (seenWorktreeTickets.has(unit)) {
2270
+ errors.push(`${basename(path)}: duplicate worktree for ${unit}`);
2271
+ }
2272
+ seenWorktreeTickets.add(unit);
2256
2273
  const ref = worktree.workspace_ref;
2257
2274
  if (
2258
2275
  typeof ref !== "string" ||
@@ -2262,20 +2279,20 @@ function validateChangeExecution(change, errors, context) {
2262
2279
  errors.push(`${basename(path)}: workspace_ref must be a portable non-absolute locator`);
2263
2280
  continue;
2264
2281
  }
2265
- if (!VALID_WORKTREE_STATUS.has(worktree.status)) {
2282
+ if (!VALID_WORKTREE_STATUS.has(worktree.status) && !(direct && worktree.status === "cancelled")) {
2266
2283
  errors.push(`${basename(path)}: invalid worktree status ${worktree.status}`);
2267
2284
  }
2268
2285
  const currentWorkspace = ref === "current";
2269
2286
  if (worktree.provider !== "git") {
2270
- errors.push(`${basename(path)}: Ticket worktree ${worktree.ticket_id} provider must be git`);
2287
+ errors.push(`${basename(path)}: Ticket worktree ${unit} provider must be git`);
2271
2288
  }
2272
- const expectedRef = `specdev-worktree/${expectedChange}/${worktree.ticket_id}`;
2289
+ const expectedRef = `specdev-worktree/${expectedChange}/${unit}`;
2273
2290
  if (!currentWorkspace && ref !== expectedRef) {
2274
2291
  errors.push(`${basename(path)}: git workspace_ref must equal ${expectedRef}`);
2275
2292
  }
2276
2293
 
2277
2294
  if ("terminal_action" in worktree) {
2278
- errors.push(`${basename(path)}: worktree ${worktree.ticket_id} obsolete terminal_action is not allowed`);
2295
+ errors.push(`${basename(path)}: worktree ${unit} obsolete terminal_action is not allowed`);
2279
2296
  }
2280
2297
  const requiredIntegrationKeys = [
2281
2298
  "owner",
@@ -2287,35 +2304,35 @@ function validateChangeExecution(change, errors, context) {
2287
2304
  ];
2288
2305
  const missingIntegrationKeys = requiredIntegrationKeys.filter((key) => !(key in worktree));
2289
2306
  if (missingIntegrationKeys.length) {
2290
- errors.push(`${basename(path)}: worktree ${worktree.ticket_id} integration contract missing ${JSON.stringify(missingIntegrationKeys)}`);
2307
+ errors.push(`${basename(path)}: worktree ${unit} integration contract missing ${JSON.stringify(missingIntegrationKeys)}`);
2291
2308
  continue;
2292
2309
  }
2293
2310
  for (const key of ["owner", "implementation_owner", "integration_owner", "base_sha", "parent_branch", "branch", "updated_at"]) {
2294
2311
  if (typeof worktree[key] !== "string" || !worktree[key].trim()) {
2295
- errors.push(`${basename(path)}: worktree ${worktree.ticket_id} ${key} is required`);
2312
+ errors.push(`${basename(path)}: worktree ${unit} ${key} is required`);
2296
2313
  }
2297
2314
  }
2298
2315
  if (typeof worktree.parent_branch !== "string" || !worktree.parent_branch.trim()) {
2299
- errors.push(`${basename(path)}: worktree ${worktree.ticket_id} parent_branch is required`);
2316
+ errors.push(`${basename(path)}: worktree ${unit} parent_branch is required`);
2300
2317
  } else if (currentWorkspace && worktree.parent_branch !== worktree.branch) {
2301
- errors.push(`${basename(path)}: current workspace ${worktree.ticket_id} branch must equal parent_branch`);
2318
+ errors.push(`${basename(path)}: current workspace ${unit} branch must equal parent_branch`);
2302
2319
  } else if (!currentWorkspace && worktree.parent_branch === worktree.branch) {
2303
- errors.push(`${basename(path)}: worktree ${worktree.ticket_id} parent_branch must differ from branch`);
2320
+ errors.push(`${basename(path)}: worktree ${unit} parent_branch must differ from branch`);
2304
2321
  }
2305
2322
 
2306
2323
  const sourceRequired = new Set(["review", "integrating", "integrated", "removed"]).has(worktree.status);
2307
2324
  if (sourceRequired && (typeof worktree.source_checkpoint !== "string" || !worktree.source_checkpoint.trim())) {
2308
- errors.push(`${basename(path)}: worktree ${worktree.ticket_id} ${worktree.status} requires source_checkpoint`);
2325
+ errors.push(`${basename(path)}: worktree ${unit} ${worktree.status} requires source_checkpoint`);
2309
2326
  } else if (
2310
2327
  worktree.source_checkpoint !== null &&
2311
2328
  (typeof worktree.source_checkpoint !== "string" || !worktree.source_checkpoint.trim())
2312
2329
  ) {
2313
- errors.push(`${basename(path)}: worktree ${worktree.ticket_id} source_checkpoint must be null or non-empty`);
2330
+ errors.push(`${basename(path)}: worktree ${unit} source_checkpoint must be null or non-empty`);
2314
2331
  }
2315
2332
 
2316
2333
  const integration = worktree.integration;
2317
2334
  if (!integration || typeof integration !== "object" || Array.isArray(integration)) {
2318
- errors.push(`${basename(path)}: worktree ${worktree.ticket_id} integration must be an object`);
2335
+ errors.push(`${basename(path)}: worktree ${unit} integration must be an object`);
2319
2336
  continue;
2320
2337
  }
2321
2338
  const requiredResultKeys = [
@@ -2339,49 +2356,48 @@ function validateChangeExecution(change, errors, context) {
2339
2356
  ];
2340
2357
  const missingResultKeys = requiredResultKeys.filter((key) => !(key in integration));
2341
2358
  if (missingResultKeys.length) {
2342
- errors.push(`${basename(path)}: worktree ${worktree.ticket_id} integration missing ${JSON.stringify(missingResultKeys)}`);
2359
+ errors.push(`${basename(path)}: worktree ${unit} integration missing ${JSON.stringify(missingResultKeys)}`);
2343
2360
  }
2344
2361
  const unexpectedResultKeys = Object.keys(integration).filter((key) => !requiredResultKeys.includes(key));
2345
2362
  if (unexpectedResultKeys.length) {
2346
- errors.push(`${basename(path)}: worktree ${worktree.ticket_id} integration has unexpected keys ${JSON.stringify(unexpectedResultKeys.sort())}`);
2363
+ errors.push(`${basename(path)}: worktree ${unit} integration has unexpected keys ${JSON.stringify(unexpectedResultKeys.sort())}`);
2347
2364
  }
2348
2365
  if (!VALID_INTEGRATION_STATUS.has(integration.status)) {
2349
- errors.push(`${basename(path)}: worktree ${worktree.ticket_id} has invalid integration status ${integration.status}`);
2366
+ errors.push(`${basename(path)}: worktree ${unit} has invalid integration status ${integration.status}`);
2350
2367
  }
2351
2368
  if (!VALID_INTEGRATION_METHOD.has(integration.method)) {
2352
- errors.push(`${basename(path)}: worktree ${worktree.ticket_id} has invalid integration method ${integration.method}`);
2369
+ errors.push(`${basename(path)}: worktree ${unit} has invalid integration method ${integration.method}`);
2353
2370
  }
2354
2371
  if (!VALID_INTEGRATION_VERIFICATION.has(integration.verification)) {
2355
- errors.push(`${basename(path)}: worktree ${worktree.ticket_id} has invalid integration verification ${integration.verification}`);
2372
+ errors.push(`${basename(path)}: worktree ${unit} has invalid integration verification ${integration.verification}`);
2356
2373
  }
2357
2374
  if (!Array.isArray(integration.conflict_paths)) {
2358
- errors.push(`${basename(path)}: worktree ${worktree.ticket_id} conflict_paths must be an array`);
2375
+ errors.push(`${basename(path)}: worktree ${unit} conflict_paths must be an array`);
2359
2376
  }
2360
2377
  if (!Number.isInteger(integration.attempts) || integration.attempts < 0) {
2361
- errors.push(`${basename(path)}: worktree ${worktree.ticket_id} attempts must be a non-negative integer`);
2378
+ errors.push(`${basename(path)}: worktree ${unit} attempts must be a non-negative integer`);
2362
2379
  }
2363
- if (
2364
- typeof integration.evidence !== "string" ||
2365
- !/^<Path>\{roots\.state\}\/specdev\/changes\/[^<]+\/evidence\/T-[0-9]{2,}\.md<\/Path>$/.test(integration.evidence)
2366
- ) {
2367
- errors.push(`${basename(path)}: worktree ${worktree.ticket_id} integration evidence must be a rooted Ticket Evidence path`);
2380
+ const expectedEvidence = `<Path>{roots.state}/specdev/changes/${expectedChange}/evidence/${unit}.md</Path>`;
2381
+ if (direct ? integration.evidence !== expectedEvidence : typeof integration.evidence !== "string" || !/^<Path>\{roots\.state\}\/specdev\/changes\/[^<]+\/evidence\/T-[0-9]{2,}\.md<\/Path>$/.test(integration.evidence)) {
2382
+ errors.push(`${basename(path)}: worktree ${unit} integration evidence must be its rooted Evidence path: ${expectedEvidence}`);
2368
2383
  }
2369
2384
  if (!new Set(["pending", "applying", "applied", "failed", "stale"]).has(integration.promotion_status)) {
2370
- errors.push(`${basename(path)}: worktree ${worktree.ticket_id} has invalid promotion_status ${integration.promotion_status}`);
2385
+ errors.push(`${basename(path)}: worktree ${unit} has invalid promotion_status ${integration.promotion_status}`);
2371
2386
  }
2372
2387
  for (const [name, suite] of [["full_suite", integration.full_suite], ["e2e", integration.e2e]]) {
2373
2388
  if (!suite || typeof suite !== "object" || Array.isArray(suite) || typeof suite.required !== "boolean" || !VALID_E2E_STATUS.has(suite.status)) {
2374
- errors.push(`${basename(path)}: worktree ${worktree.ticket_id} ${name} contract is invalid`);
2389
+ errors.push(`${basename(path)}: worktree ${unit} ${name} contract is invalid`);
2375
2390
  continue;
2376
2391
  }
2377
2392
  const unexpectedSuiteKeys = Object.keys(suite).filter((key) => !new Set(["required", "status", "reason", "evidence"]).has(key));
2378
- if (unexpectedSuiteKeys.length) errors.push(`${basename(path)}: worktree ${worktree.ticket_id} ${name} has unexpected keys ${JSON.stringify(unexpectedSuiteKeys.sort())}`);
2393
+ if (unexpectedSuiteKeys.length) errors.push(`${basename(path)}: worktree ${unit} ${name} has unexpected keys ${JSON.stringify(unexpectedSuiteKeys.sort())}`);
2379
2394
  if (suite.required === false && (suite.status !== "not-required" || typeof suite.reason !== "string" || !suite.reason.trim())) {
2380
- errors.push(`${basename(path)}: worktree ${worktree.ticket_id} non-required ${name} needs not-required status and reason`);
2395
+ errors.push(`${basename(path)}: worktree ${unit} non-required ${name} needs not-required status and reason`);
2381
2396
  }
2382
2397
  if (suite.required === true && suite.status === "not-required") {
2383
- errors.push(`${basename(path)}: worktree ${worktree.ticket_id} required ${name} cannot be not-required`);
2398
+ errors.push(`${basename(path)}: worktree ${unit} required ${name} cannot be not-required`);
2384
2399
  }
2400
+ if (direct && suite.required === true && suite.status === "passed" && (typeof suite.evidence !== "string" || !suite.evidence.trim())) errors.push(`Direct Spec passed ${name} requires actual evidence`);
2385
2401
  }
2386
2402
  const e2e = integration.e2e;
2387
2403
  if (
@@ -2391,65 +2407,65 @@ function validateChangeExecution(change, errors, context) {
2391
2407
  typeof e2e.required !== "boolean" ||
2392
2408
  !VALID_E2E_STATUS.has(e2e.status)
2393
2409
  ) {
2394
- errors.push(`${basename(path)}: worktree ${worktree.ticket_id} integration e2e contract is invalid`);
2410
+ errors.push(`${basename(path)}: worktree ${unit} integration e2e contract is invalid`);
2395
2411
  } else {
2396
2412
  const unexpectedE2eKeys = Object.keys(e2e).filter((key) => !new Set(["required", "status", "reason", "evidence"]).has(key));
2397
2413
  if (unexpectedE2eKeys.length) {
2398
- errors.push(`${basename(path)}: worktree ${worktree.ticket_id} integration e2e has unexpected keys ${JSON.stringify(unexpectedE2eKeys.sort())}`);
2414
+ errors.push(`${basename(path)}: worktree ${unit} integration e2e has unexpected keys ${JSON.stringify(unexpectedE2eKeys.sort())}`);
2399
2415
  }
2400
2416
  if (e2e.required === false && e2e.status !== "not-required") {
2401
- errors.push(`${basename(path)}: worktree ${worktree.ticket_id} non-required E2E must use status=not-required`);
2417
+ errors.push(`${basename(path)}: worktree ${unit} non-required E2E must use status=not-required`);
2402
2418
  }
2403
2419
  if (e2e.required === true && e2e.status === "not-required") {
2404
- errors.push(`${basename(path)}: worktree ${worktree.ticket_id} required E2E cannot be not-required`);
2420
+ errors.push(`${basename(path)}: worktree ${unit} required E2E cannot be not-required`);
2405
2421
  }
2406
2422
  if (
2407
2423
  e2e.required === true &&
2408
2424
  e2e.status === "passed" &&
2409
2425
  (typeof e2e.evidence !== "string" || !e2e.evidence.trim())
2410
2426
  ) {
2411
- errors.push(`${basename(path)}: worktree ${worktree.ticket_id} passed required E2E needs evidence`);
2427
+ errors.push(`${basename(path)}: worktree ${unit} passed required E2E needs evidence`);
2412
2428
  }
2413
2429
  }
2414
2430
  const integrationLifecycleActive = new Set(["integrating", "integrated", "removed"]).has(worktree.status);
2415
2431
  if (integrationLifecycleActive) {
2416
2432
  if (typeof integration.parent_before_sha !== "string" || !integration.parent_before_sha.trim()) {
2417
- errors.push(`${basename(path)}: worktree ${worktree.ticket_id} ${worktree.status} requires parent_before_sha`);
2433
+ errors.push(`${basename(path)}: worktree ${unit} ${worktree.status} requires parent_before_sha`);
2418
2434
  }
2419
2435
  if (typeof integration.source_sha !== "string" || !integration.source_sha.trim()) {
2420
- errors.push(`${basename(path)}: worktree ${worktree.ticket_id} ${worktree.status} requires source_sha`);
2436
+ errors.push(`${basename(path)}: worktree ${unit} ${worktree.status} requires source_sha`);
2421
2437
  } else if (integration.source_sha !== worktree.source_checkpoint) {
2422
- errors.push(`${basename(path)}: worktree ${worktree.ticket_id} source_sha must equal source_checkpoint`);
2438
+ errors.push(`${basename(path)}: worktree ${unit} source_sha must equal source_checkpoint`);
2423
2439
  }
2424
2440
  if (currentWorkspace) {
2425
2441
  if (integration.candidate_sha !== null || integration.candidate_tree_sha !== null || integration.candidate_branch !== null || integration.candidate_workspace_ref !== null) {
2426
- errors.push(`${basename(path)}: current workspace ${worktree.ticket_id} cannot record candidate workspace fields`);
2442
+ errors.push(`${basename(path)}: current workspace ${unit} cannot record candidate workspace fields`);
2427
2443
  }
2428
2444
  if (integration.method !== "direct-parent") {
2429
- errors.push(`${basename(path)}: current workspace ${worktree.ticket_id} requires integration.method=direct-parent`);
2445
+ errors.push(`${basename(path)}: current workspace ${unit} requires integration.method=direct-parent`);
2430
2446
  }
2431
2447
  } else {
2432
2448
  if (typeof integration.candidate_sha !== "string" || !integration.candidate_sha.trim()) {
2433
- errors.push(`${basename(path)}: worktree ${worktree.ticket_id} ${worktree.status} requires candidate_sha`);
2449
+ errors.push(`${basename(path)}: worktree ${unit} ${worktree.status} requires candidate_sha`);
2434
2450
  }
2435
- const expectedCandidateBranch = `speculo/integration/${expectedChange}/${worktree.ticket_id}`;
2451
+ const expectedCandidateBranch = `speculo/integration/${expectedChange}/${unit}`;
2436
2452
  if (integration.candidate_branch !== expectedCandidateBranch) {
2437
- errors.push(`${basename(path)}: worktree ${worktree.ticket_id} candidate_branch must equal ${expectedCandidateBranch}`);
2453
+ errors.push(`${basename(path)}: worktree ${unit} candidate_branch must equal ${expectedCandidateBranch}`);
2438
2454
  }
2439
- const expectedCandidateRef = `specdev-worktree/.integration/${expectedChange}/${worktree.ticket_id}`;
2455
+ const expectedCandidateRef = `specdev-worktree/.integration/${expectedChange}/${unit}`;
2440
2456
  if (integration.candidate_workspace_ref !== expectedCandidateRef) {
2441
- errors.push(`${basename(path)}: worktree ${worktree.ticket_id} candidate_workspace_ref must equal ${expectedCandidateRef}`);
2457
+ errors.push(`${basename(path)}: worktree ${unit} candidate_workspace_ref must equal ${expectedCandidateRef}`);
2442
2458
  }
2443
2459
  if (!new Set(["fast-forward", "merge-commit"]).has(integration.method)) {
2444
- errors.push(`${basename(path)}: worktree ${worktree.ticket_id} ${worktree.status} requires an integration method`);
2460
+ errors.push(`${basename(path)}: worktree ${unit} ${worktree.status} requires an integration method`);
2445
2461
  }
2446
2462
  }
2447
2463
  if (!Number.isInteger(integration.attempts) || integration.attempts < 1) {
2448
- errors.push(`${basename(path)}: worktree ${worktree.ticket_id} ${worktree.status} requires attempts >= 1`);
2464
+ errors.push(`${basename(path)}: worktree ${unit} ${worktree.status} requires attempts >= 1`);
2449
2465
  }
2450
2466
  }
2451
2467
  if (worktree.status === "integrating" && integration.status !== "candidate") {
2452
- errors.push(`${basename(path)}: integrating worktree ${worktree.ticket_id} requires integration.status=candidate`);
2468
+ errors.push(`${basename(path)}: integrating worktree ${unit} requires integration.status=candidate`);
2453
2469
  }
2454
2470
  if (new Set(["integrated", "removed"]).has(worktree.status)) {
2455
2471
  if (
@@ -2459,31 +2475,32 @@ function validateChangeExecution(change, errors, context) {
2459
2475
  typeof integration.result_sha !== "string" ||
2460
2476
  !integration.result_sha.trim() ||
2461
2477
  (currentWorkspace ? integration.result_sha !== worktree.source_checkpoint : integration.result_sha !== integration.candidate_sha) ||
2478
+ (direct && (integration.promotion_status !== "applied" || !new Set(["not-required", "passed"]).has(integration.full_suite?.status))) ||
2462
2479
  !e2e ||
2463
2480
  !new Set(["not-required", "passed"]).has(e2e.status)
2464
2481
  ) {
2465
- errors.push(`${basename(path)}: ${worktree.status} worktree ${worktree.ticket_id} requires passed candidate/result/E2E state`);
2482
+ errors.push(`${basename(path)}: ${worktree.status} worktree ${unit} requires passed candidate/result/E2E state`);
2466
2483
  }
2467
2484
  if (currentWorkspace && integration.method === "direct-parent") {
2468
2485
  if (integration.candidate_sha !== null || integration.candidate_tree_sha !== null || integration.candidate_branch !== null || integration.candidate_workspace_ref !== null) {
2469
- errors.push(`${basename(path)}: current workspace ${worktree.ticket_id} direct-parent cannot record candidate fields`);
2486
+ errors.push(`${basename(path)}: current workspace ${unit} direct-parent cannot record candidate fields`);
2470
2487
  }
2471
2488
  if (integration.result_sha !== worktree.source_checkpoint) {
2472
- errors.push(`${basename(path)}: current workspace ${worktree.status} ${worktree.ticket_id} result_sha must equal source_checkpoint`);
2489
+ errors.push(`${basename(path)}: current workspace ${worktree.status} ${unit} result_sha must equal source_checkpoint`);
2473
2490
  }
2474
2491
  } else if (integration.method === "fast-forward") {
2475
2492
  if (integration.candidate_sha !== worktree.source_checkpoint) {
2476
- errors.push(`${basename(path)}: ${worktree.status} worktree ${worktree.ticket_id} fast-forward candidate/result must equal source_checkpoint`);
2493
+ errors.push(`${basename(path)}: ${worktree.status} worktree ${unit} fast-forward candidate/result must equal source_checkpoint`);
2477
2494
  }
2478
2495
  if (Array.isArray(integration.conflict_paths) && integration.conflict_paths.length > 0) {
2479
- errors.push(`${basename(path)}: ${worktree.status} worktree ${worktree.ticket_id} fast-forward cannot record conflict_paths`);
2496
+ errors.push(`${basename(path)}: ${worktree.status} worktree ${unit} fast-forward cannot record conflict_paths`);
2480
2497
  }
2481
2498
  } else if (
2482
2499
  integration.method === "merge-commit" &&
2483
2500
  typeof integration.candidate_sha === "string" &&
2484
2501
  (integration.candidate_sha === worktree.source_checkpoint || integration.candidate_sha === integration.parent_before_sha)
2485
2502
  ) {
2486
- errors.push(`${basename(path)}: ${worktree.status} worktree ${worktree.ticket_id} merge-commit candidate must be a distinct checkpoint`);
2503
+ errors.push(`${basename(path)}: ${worktree.status} worktree ${unit} merge-commit candidate must be a distinct checkpoint`);
2487
2504
  }
2488
2505
  }
2489
2506
  }
@@ -2491,8 +2508,8 @@ function validateChangeExecution(change, errors, context) {
2491
2508
  }
2492
2509
 
2493
2510
  function validateCurrentWorkspaceExecution(changeStatus, goalPlan, errors) {
2494
- if (goalPlan?.meta.ticket_workspace_policy !== "current") return;
2495
- const entries = Array.isArray(changeStatus?.worktrees) ? changeStatus.worktrees : [];
2511
+ const allEntries = Array.isArray(changeStatus?.worktrees) ? changeStatus.worktrees : [];
2512
+ const entries = goalPlan?.meta.ticket_workspace_policy === "current" ? allEntries : allEntries.filter(entry => entry.workspace_ref === "current");
2496
2513
  const active = [];
2497
2514
  for (const worktree of entries) {
2498
2515
  if (worktree.workspace_ref !== "current") {
@@ -2502,7 +2519,7 @@ function validateCurrentWorkspaceExecution(changeStatus, goalPlan, errors) {
2502
2519
  errors.push(`${worktree.ticket_id}: current workspace execution must commit directly on parent_branch`);
2503
2520
  }
2504
2521
  if (new Set(["active", "review", "integrating", "blocked"]).has(worktree.status)) {
2505
- active.push(worktree.ticket_id);
2522
+ active.push(worktree.ticket_id ?? worktree.purpose);
2506
2523
  }
2507
2524
  const integration = worktree.integration;
2508
2525
  if (integration && typeof integration === "object" && !Array.isArray(integration)) {
@@ -2549,7 +2566,7 @@ function validateGitEvidence(repoRoot, changeStatus, errors) {
2549
2566
  }
2550
2567
  const currentBranch = gitOutput(resolvedRoot, ["branch", "--show-current"]);
2551
2568
  for (const worktree of Array.isArray(changeStatus?.worktrees) ? changeStatus.worktrees : []) {
2552
- const label = String(worktree.ticket_id ?? "worktree");
2569
+ const label = String(worktree.ticket_id ?? worktree.purpose ?? "worktree");
2553
2570
  for (const [name, sha] of [
2554
2571
  ["base_sha", worktree.base_sha],
2555
2572
  ["source_checkpoint", worktree.source_checkpoint],
@@ -2586,6 +2603,36 @@ function validateGitEvidence(repoRoot, changeStatus, errors) {
2586
2603
  if (gitOutput(candidate,["rev-parse","HEAD"]) !== worktree.integration.candidate_sha || gitOutput(candidate,["rev-parse","HEAD^{tree}"]) !== worktree.integration.candidate_tree_sha) errors.push(`${label}: actual candidate workspace differs from the recorded checkpoint/tree`);
2587
2604
  } catch(error) { errors.push(`${label}: actual candidate isolation is missing: ${error.message}`); }
2588
2605
  }
2606
+ if (worktree.purpose === "direct-spec") {
2607
+ const integration = worktree.integration;
2608
+ if (worktree.source_checkpoint && gitOutput(resolvedRoot, ["rev-parse", worktree.branch]) !== worktree.source_checkpoint && worktree.status !== "removed") errors.push(`${label}: source branch tip differs from source_checkpoint`);
2609
+ if (worktree.source_checkpoint && !gitSucceeds(resolvedRoot, ["merge-base", "--is-ancestor", worktree.base_sha, worktree.source_checkpoint])) errors.push(`${label}: source_checkpoint must descend from base_sha`);
2610
+ if (worktree.workspace_ref !== "current" && new Set(["review", "integrating", "integrated"]).has(worktree.status)) {
2611
+ try {
2612
+ const source = runtime.safeWorkPathSync(resolvedRoot, worktree.workspace_ref, {mustExist:true,type:"directory"});
2613
+ if (gitOutput(source, ["status", "--porcelain"])) errors.push(`${label}: source worktree is dirty`);
2614
+ } catch (error) { errors.push(`${label}: source worktree missing before cleanup: ${error.message}`); }
2615
+ }
2616
+ if (integration?.candidate_sha) {
2617
+ if (gitOutput(resolvedRoot, ["rev-parse", `${integration.candidate_sha}^{tree}`]) !== integration.candidate_tree_sha) errors.push(`${label}: candidate tree differs from recorded tree`);
2618
+ for (const sha of [integration.parent_before_sha, worktree.source_checkpoint]) {
2619
+ if (sha && !gitSucceeds(resolvedRoot, ["merge-base", "--is-ancestor", sha, integration.candidate_sha])) errors.push(`${label}: candidate must contain parent and source checkpoints`);
2620
+ }
2621
+ }
2622
+ const parent = gitOutput(resolvedRoot, ["rev-parse", worktree.parent_branch]);
2623
+ if (worktree.status === "integrating" && worktree.workspace_ref !== "current") {
2624
+ try {
2625
+ const candidate = runtime.safeWorkPathSync(resolvedRoot, integration.candidate_workspace_ref, {mustExist:true,type:"directory"});
2626
+ if (gitOutput(candidate, ["rev-parse", "--path-format=absolute", "--git-common-dir"]) !== gitOutput(resolvedRoot, ["rev-parse", "--path-format=absolute", "--git-common-dir"]) || gitOutput(candidate, ["branch", "--show-current"]) !== integration.candidate_branch || gitOutput(candidate, ["status", "--porcelain"])) errors.push(`${label}: candidate must be a clean worktree of the recorded repository and branch`);
2627
+ } catch (error) { errors.push(`${label}: candidate cannot be inspected: ${error.message}`); }
2628
+ }
2629
+ if (worktree.status === "integrating" && parent !== integration?.parent_before_sha) errors.push(`${label}: parent HEAD drifted; candidate must be marked stale and rebuilt`);
2630
+ if (new Set(["failed", "stale"]).has(integration?.status) && parent === integration.candidate_sha && parent !== integration.parent_before_sha) errors.push(`${label}: failed or stale candidate has already advanced the parent; reconcile before continuing`);
2631
+ if (new Set(["integrated", "removed"]).has(worktree.status)) {
2632
+ if (parent !== integration?.result_sha) errors.push(`${label}: parent branch HEAD must equal recorded result_sha`);
2633
+ if (!gitSucceeds(resolvedRoot, ["merge-base", "--is-ancestor", worktree.source_checkpoint, parent])) errors.push(`${label}: parent must contain source_checkpoint`);
2634
+ }
2635
+ }
2589
2636
  if (new Set(["integrated", "removed"]).has(worktree.status) && gitOutput(resolvedRoot, ["status", "--porcelain"])) {
2590
2637
  errors.push(`${label}: repository is dirty while Ticket is recorded as ${worktree.status}`);
2591
2638
  }
@@ -3083,7 +3130,7 @@ function validateChange(change, stage = null, repoRoot = null, engineeringKind =
3083
3130
  validateReviews(change, stage === "review", errors);
3084
3131
  validatePrototypes(change, stage === "prototype", errors, prototypeId, new Set(["spec", "tickets", "goal-plan", "implement", "review", "complete"]).has(stage), workspace.context);
3085
3132
  if (new Set(["tickets", "goal-plan", "implement", "review", "complete"]).has(stage)) runDesignCheck("--handoff", change, errors, workspace.context);
3086
- validateChangeLearning(change, stage === "learn-change", errors);
3133
+ validateChangeLearning(change, stage === "learn", errors);
3087
3134
  const retro = validateRecords(change, "retrospective", parseFrontmatter);
3088
3135
  const prs = validateRecords(change, "pull-request", parseFrontmatter);
3089
3136
  errors.push(...retro.errors, ...prs.errors);
@@ -3206,10 +3253,17 @@ function validateChange(change, stage = null, repoRoot = null, engineeringKind =
3206
3253
  }
3207
3254
  }
3208
3255
  }
3209
- validateCurrentWorkspaceExecution(changeStatus, goalPlan, errors);
3210
3256
  }
3257
+ validateCurrentWorkspaceExecution(changeStatus, goalPlan, errors);
3258
+ const directExecution = changeStatus?.worktrees?.find(entry => entry.purpose === "direct-spec");
3259
+ if (directExecution && workspace.config) errors.push(...validateExecutionPolicy({meta:{concurrent_implementation_limit:1,integration_attempt_limit:directExecution.integration_attempt_limit}},workspace.config).map(error => `Direct Spec: ${error}`));
3260
+ if (directExecution && (ticketMode || isParentImplementation)) errors.push("Direct Spec execution cannot coexist with Ticket or parent implementation artifacts");
3261
+ if (directExecution && (stage === "complete" || changeStatus.outcome === "completed") && !new Set(["integrated", "removed"]).has(directExecution.status)) errors.push("Direct Spec completion requires integrated or removed execution; active or failed candidates cannot complete");
3211
3262
  validateGitEvidence(stage === "complete" ? workspace.context.projectRoot : repoRoot, changeStatus, errors);
3212
3263
 
3264
+ if (stage === "implement" && spec && !ticketMode && (spec.meta.ready_for_tickets !== true || spec.meta.status !== "ready")) errors.push("Direct Spec implementation requires a ready Spec with ready_for_tickets=true");
3265
+ if (spec?.meta.ready_for_tickets === true && !/\bAC-\d+\b/.test(spec.body)) errors.push("ready Spec must define at least one AC-### acceptance contract");
3266
+
3213
3267
  if (stage === "complete" && spec && !ticketMode && !isParentImplementation && !retroOnly) errors.push(...validateAcceptanceEvidence(change,"evidence/direct-spec.md",[...new Set(spec.body.match(/\bAC-\d+\b/g) ?? [])]));
3214
3268
 
3215
3269
  if (spec && ticketMode) {
@@ -3297,7 +3351,7 @@ function validateChange(change, stage = null, repoRoot = null, engineeringKind =
3297
3351
  continue;
3298
3352
  }
3299
3353
  const text = readText(evidence);
3300
- const currentWorkspace = goalPlan?.meta.ticket_workspace_policy === "current";
3354
+ const currentWorkspace = changeStatus?.worktrees?.find(entry => entry.ticket_id === ticketId)?.workspace_ref === "current";
3301
3355
  const requiredHeadings = [
3302
3356
  "## 2. Lead Dispatch And Candidate Return",
3303
3357
  "## 3. 修改范围与路径所有权",
@@ -82,11 +82,9 @@
82
82
  },
83
83
  {
84
84
  "id": "learn",
85
- "after": [
86
- "verify"
87
- ],
85
+ "after": [],
88
86
  "inputs": [
89
- "verification"
87
+ "learning-request"
90
88
  ],
91
89
  "outputs": [
92
90
  "learning"
@@ -103,7 +101,7 @@
103
101
  "specdev-g-grill-with-docs/SKILL.md",
104
102
  "specdev-i-implement/SKILL.md",
105
103
  "specdev-i-init-setup/SKILL.md",
106
- "specdev-l-learn-change/SKILL.md",
104
+ "specdev-l-learn/SKILL.md",
107
105
  "specdev-p-goal-plan/SKILL.md",
108
106
  "specdev-p-prototype/SKILL.md",
109
107
  "specdev-r-retro/SKILL.md",
@@ -134,7 +132,7 @@
134
132
  "specdev-d-diagnose-bugs/SKILL.md": [],
135
133
  "specdev-g-grill-with-docs/SKILL.md": [],
136
134
  "specdev-i-init-setup/SKILL.md": [],
137
- "specdev-l-learn-change/SKILL.md": [],
135
+ "specdev-l-learn/SKILL.md": [],
138
136
  "specdev-p-prototype/SKILL.md": [],
139
137
  "specdev-r-retro/SKILL.md": [],
140
138
  "specdev-r-review-architecture/SKILL.md": [],
@@ -48,8 +48,8 @@ UI diff 按 <Path>{roots.workflows}/specdev/common/rules/design-contract.md</Pat
48
48
 
49
49
  ## 编排与执行上下文
50
50
 
51
- Lead 自行执行或当前阶段需要交接任务时,读取 <Path>{roots.references}/coordination/coordination-contract.md</Path>,按用户指定的 Lead、Orca、Herdr 或无头 CLI 方式操作。只读取所选工具的当前指南;不固定编程工具或模型。
51
+ 进入实现或审查时读取 <Path>{roots.references}/coordination/coordination-contract.md</Path>;未明确指定时按当前宿主正常执行,App、CLI 等均可,subagent/agent team 属于宿主能力。明确指定 CLI 交互、Herdr 等方式时按选择执行并展开对应参考;编排与 current/worktree 独立,已有授权和选择直接复用。只读取所选工具的当前指南,不固定工具或模型。
52
52
 
53
- `lead` 上下文维护原业务状态和 Evidence;`task` 上下文只处理 Lead 指定目标和阶段,执行同一方法并返回候选、findings、检查和未验证项,不修改正式 Ticket、Map、Goal、Evidence,不递归启动 Goal 或交接其他任务。未曝光到宿主列表不影响读取真实入口。
53
+ `lead` 上下文维护原业务状态和 Evidence;`task` 上下文只处理 Lead 指定目标和阶段,执行同一方法并返回候选、findings、检查和未验证项,不修改正式 Ticket、Map、Goal、Evidence,不递归启动 Goal 或擅自跨 CLI 派单;宿主原生协作遵守原写集和单 writer 约束。未曝光到宿主列表不影响读取真实入口。
54
54
 
55
55
  P 在已授权 run/resume 中完成当前实现、审查、验证和同范围修复后继续下一 Ready 任务,不逐票询问“继续”。只读分析或 plan 不执行。独立审查 required 时不能以 Lead 自审替代。
@@ -16,17 +16,17 @@ metadata:
16
16
 
17
17
  先读 `<Path>{roots.workflows}/specdev/common/rules/activation-and-memory.md</Path>`;从 README/原 Goal 或当前 tickets-map/父 map 定位所需范围、本票、上游约束、项目 Skill 与执行策略。仅展开当前 Ticket 的正文、直接依赖、适用 Skill 和必要恢复证据,不整读知识库。
18
18
 
19
- 轻量实现可读取 README 的唯一 scope/acceptance/verification 规格章节,等价于获批低风险 Direct Spec;保留明确范围、可判定 AC、真实验证及停止条件。存在独立 <Path>{roots.state}/specdev/changes/{change}/spec.md</Path> 时优先读取并拒绝双权威。无需独立票时不造 Ticket/Map;UI 的 DESIGN、批准和真实浏览器门保持。
19
+ 轻量实现可读取 README 的唯一 scope/acceptance/verification 规格章节,等价于获批轻量 Direct Spec;独立完整 Spec 可按其全部风险门直接实施;保留明确范围、可判定 AC、真实验证及停止条件。存在独立 <Path>{roots.state}/specdev/changes/{change}/spec.md</Path> 时优先读取并拒绝双权威。无需独立票时不造 Ticket/Map;UI 的 DESIGN、批准和真实浏览器门保持。
20
20
 
21
21
  ## 当前票的执行入口
22
22
 
23
- 执行必须读取 `<Path>{roots.workflows}/specdev/specdev-i-implement/references/implementation-procedure.md</Path>` 和 `<Path>{roots.workflows}/specdev/specdev-i-implement/references/execution-preflight.md</Path>`;执行过程按当前阶段路由设计、TDD、双轴审查和 Evidence;预检单独拥有 Git、workspace、Ready 与授权检查。两者必读,分支参考只在对应 workspace/风险/阶段进入时加载。
23
+ 执行先读取 <Path>{roots.workflows}/specdev/common/rules/execution-policy.md</Path>,确定 Spec/Ticket、执行方式与工作区三个独立选择;完整获批 Spec 不因范围较大而强制拆票,已有 Ready Ticket 不强制补 Goal。执行必须读取 `<Path>{roots.workflows}/specdev/specdev-i-implement/references/implementation-procedure.md</Path>` 和 `<Path>{roots.workflows}/specdev/specdev-i-implement/references/execution-preflight.md</Path>`;执行过程按当前阶段路由设计、TDD、双轴审查和 Evidence;预检单独拥有 Git、workspace、Ready 与授权检查。两者必读,分支参考只在对应 workspace/风险/阶段进入时加载。
24
24
 
25
25
  UI Ticket 或 Direct Spec 必须读取 <Path>{roots.workflows}/specdev/common/rules/design-contract.md</Path> 与当前批准 DESIGN,核验 design_ref(revision/digest/pages/acceptance)、原型和基线。页面任务按已批准规则实现,不重新选择模板;实际实现证据写入 `Design Conformance`,原型截图不替代生产页面验证。
26
26
 
27
27
  1. 核验 Ready、授权、owner、真实源与 map 基线;新增计划型票按 `<Path>{roots.workflows}/specdev/common/rules/skill-invocation.md</Path>` 实际调用绑定能力并记录证据。票缺调用契约时先由 Lead 补齐,不猜测。
28
28
  2. 保持 codebase-design、design-it-twice 和 TDD 的适用门禁;进入对应实现步骤再读其 reference。派单时调用 共享编排方法;Lead 是唯一 SpecDev 状态写入者。
29
- 3. current 模式串行使用当前 workspace;required 模式使用独立 worktree,E2E 由 Lead 在 parent-candidate 完成。实现 commit、父分支推进和集成均需真实授权。
29
+ 3. Ticket 和 Direct Spec 均可在 current 模式串行使用当前 workspace,或在 required 模式使用独立 worktree,E2E 由 Lead 在 current 或 required 的 parent-candidate 完成。实现 commit、父分支推进和集成均需真实授权。
30
30
  4. 安全、并发、公共接口、数据迁移或用户要求全面审查时展开 `<Path>{roots.workflows}/specdev/common/skills/code-review/references/risk-review.md</Path>`;不为省上下文删减必要检查或限制发现数量。
31
31
  5. 写 Evidence、运行适用校验并回读后才推进状态;必需 Skill 失败、验收失败、越界或归属冲突暂停本票及依赖它的分支。无关工作仍由总控继续,不接管他人事务。
32
32
 
@@ -42,8 +42,8 @@ UI Ticket 或 Direct Spec 必须读取 <Path>{roots.workflows}/specdev/common/ru
42
42
 
43
43
  ## 编排与执行上下文
44
44
 
45
- Lead 自行执行或当前阶段需要交接任务时,读取 <Path>{roots.references}/coordination/coordination-contract.md</Path>,按用户指定的 Lead、Orca、Herdr 或无头 CLI 方式操作。只读取所选工具的当前指南;不固定编程工具或模型。
45
+ 进入实现或审查时读取 <Path>{roots.references}/coordination/coordination-contract.md</Path>;未明确指定时按当前宿主正常执行,App、CLI 等均可,subagent/agent team 属于宿主能力。明确指定 CLI 交互、Herdr 等方式时按选择执行并展开对应参考;编排与 current/worktree 独立,已有授权和选择直接复用。只读取所选工具的当前指南,不固定工具或模型。
46
46
 
47
- `lead` 上下文维护原业务状态和 Evidence;`task` 上下文只处理 Lead 指定目标和阶段,执行同一方法并返回候选、findings、检查和未验证项,不修改正式 Ticket、Map、Goal、Evidence,不递归启动 Goal 或交接其他任务。未曝光到宿主列表不影响读取真实入口。
47
+ `lead` 上下文维护原业务状态和 Evidence;`task` 上下文只处理 Lead 指定目标和阶段,执行同一方法并返回候选、findings、检查和未验证项,不修改正式 Ticket、Map、Goal、Evidence,不递归启动 Goal 或擅自跨 CLI 派单;宿主原生协作遵守原写集和单 writer 约束。未曝光到宿主列表不影响读取真实入口。
48
48
 
49
49
  P 在已授权 run/resume 中完成当前实现、审查、验证和同范围修复后继续下一 Ready 任务,不逐票询问“继续”。只读分析或 plan 不执行。独立审查 required 时不能以 Lead 自审替代。