coding-agent-harness 1.0.2 → 1.0.4

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 (177) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/CONTRIBUTING.md +98 -0
  3. package/README.md +211 -86
  4. package/README.zh-CN.md +54 -34
  5. package/SKILL.md +25 -18
  6. package/docs-release/README.md +9 -5
  7. package/docs-release/architecture/overview.md +17 -5
  8. package/docs-release/architecture/overview.zh-CN.md +9 -5
  9. package/docs-release/assets/dashboard-overview.png +0 -0
  10. package/docs-release/guides/agent-installation.en-US.md +31 -8
  11. package/docs-release/guides/agent-installation.md +34 -9
  12. package/docs-release/guides/contributing.md +100 -0
  13. package/docs-release/guides/contributing.zh-CN.md +99 -0
  14. package/docs-release/guides/document-audience-and-surfaces.en-US.md +3 -2
  15. package/docs-release/guides/document-audience-and-surfaces.md +3 -2
  16. package/docs-release/guides/full-legacy-migration-subagent-strategy.md +2 -2
  17. package/docs-release/guides/full-legacy-migration-subagent-strategy.zh-CN.md +2 -2
  18. package/docs-release/guides/legacy-migration-agent-prompt.md +0 -11
  19. package/docs-release/guides/legacy-migration-agent-prompt.zh-CN.md +0 -11
  20. package/docs-release/guides/migration-playbook.en-US.md +14 -15
  21. package/docs-release/guides/migration-playbook.md +14 -15
  22. package/docs-release/guides/parent-control-repository-pattern.en-US.md +7 -5
  23. package/docs-release/guides/parent-control-repository-pattern.md +7 -5
  24. package/docs-release/guides/preset-development.md +214 -0
  25. package/docs-release/guides/repository-operating-models.en-US.md +5 -4
  26. package/docs-release/guides/repository-operating-models.md +5 -4
  27. package/docs-release/guides/task-state-machine.en-US.md +207 -0
  28. package/docs-release/guides/task-state-machine.md +214 -0
  29. package/docs-release/intl/en-US.md +1 -1
  30. package/docs-release/intl/zh-CN.md +1 -1
  31. package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/findings.md +7 -0
  32. package/package.json +8 -3
  33. package/presets/legacy-migration/checks/preset-check.mjs +3 -0
  34. package/presets/legacy-migration/preset.yaml +134 -0
  35. package/presets/legacy-migration/scripts/plan-work-queue.mjs +4 -0
  36. package/presets/legacy-migration/scripts/scaffold-task-contracts.mjs +4 -0
  37. package/presets/legacy-migration/templates/execution_strategy.append.md +18 -0
  38. package/presets/legacy-migration/templates/findings.seed.md +17 -0
  39. package/presets/legacy-migration/templates/review.seed.md +12 -0
  40. package/presets/legacy-migration/templates/task_plan.append.md +9 -0
  41. package/presets/legacy-migration/templates/visual_map.append.md +12 -0
  42. package/presets/legacy-migration/workbench/dashboard-panels.yaml +2 -0
  43. package/presets/legacy-migration/workbench/migration-queue.schema.json +23 -0
  44. package/presets/lesson-sedimentation/preset.yaml +23 -0
  45. package/presets/lesson-sedimentation/templates/prompt.md +23 -0
  46. package/presets/module/preset.yaml +25 -0
  47. package/presets/module/templates/execution_strategy.append.md +8 -0
  48. package/presets/module/templates/task_plan.append.md +17 -0
  49. package/presets/standard-task/preset.yaml +31 -0
  50. package/presets/standard-task/templates/task_plan.append.md +7 -0
  51. package/references/adversarial-review-standard.md +2 -2
  52. package/references/agents-md-pattern.md +2 -2
  53. package/references/delivery-operating-model-standard.md +3 -3
  54. package/references/docs-directory-standard.md +6 -7
  55. package/references/harness-ledger.md +53 -96
  56. package/references/lessons-governance.md +88 -93
  57. package/references/module-parallel-standard.md +14 -14
  58. package/references/planning-loop.md +12 -6
  59. package/references/pull-request-standard.md +118 -0
  60. package/references/repo-governance-standard.md +11 -2
  61. package/references/review-routing-standard.md +7 -1
  62. package/references/ssot-governance.md +67 -59
  63. package/references/taskr-gap-analysis.md +600 -0
  64. package/references/walkthrough-closeout.md +7 -7
  65. package/scripts/check-harness.mjs +40 -301
  66. package/scripts/commands/dashboard-command.mjs +67 -0
  67. package/scripts/commands/migration-command.mjs +96 -0
  68. package/scripts/commands/preset-command.mjs +73 -0
  69. package/scripts/commands/task-command.mjs +327 -0
  70. package/scripts/harness.mjs +55 -260
  71. package/scripts/lib/capability-registry.mjs +66 -8
  72. package/scripts/lib/check-module-parallel.mjs +237 -0
  73. package/scripts/lib/check-profiles.mjs +61 -153
  74. package/scripts/lib/check-task-contracts.mjs +47 -0
  75. package/scripts/lib/core-shared.mjs +10 -0
  76. package/scripts/lib/dashboard-data.mjs +29 -6
  77. package/scripts/lib/dashboard-workbench.mjs +52 -12
  78. package/scripts/lib/dashboard-writer.mjs +14 -2
  79. package/scripts/lib/git-status-summary.mjs +46 -0
  80. package/scripts/lib/governance-index-generator.mjs +174 -0
  81. package/scripts/lib/governance-sync.mjs +514 -0
  82. package/scripts/lib/governance-table-boundary.mjs +175 -0
  83. package/scripts/lib/harness-core.mjs +5 -0
  84. package/scripts/lib/lesson-maintenance.mjs +36 -29
  85. package/scripts/lib/migration-support.mjs +1 -1
  86. package/scripts/lib/preset-audit-contracts.mjs +37 -0
  87. package/scripts/lib/preset-engine.mjs +497 -0
  88. package/scripts/lib/preset-registry.mjs +627 -0
  89. package/scripts/lib/preset-resource-contracts.mjs +83 -0
  90. package/scripts/lib/review-confirm-git-gate.mjs +248 -0
  91. package/scripts/lib/status-dashboard-renderer.mjs +102 -0
  92. package/scripts/lib/subagent-authorization-audit.mjs +196 -0
  93. package/scripts/lib/task-completion-consistency.mjs +16 -0
  94. package/scripts/lib/task-index.mjs +93 -0
  95. package/scripts/lib/task-lesson-candidates.mjs +242 -0
  96. package/scripts/lib/task-lesson-sedimentation.mjs +326 -0
  97. package/scripts/lib/task-lifecycle/review-confirm.mjs +101 -0
  98. package/scripts/lib/task-lifecycle/review-gates.mjs +70 -0
  99. package/scripts/lib/task-lifecycle/text-utils.mjs +24 -0
  100. package/scripts/lib/task-lifecycle.mjs +297 -403
  101. package/scripts/lib/task-review-model.mjs +469 -0
  102. package/scripts/lib/task-scanner.mjs +130 -236
  103. package/scripts/lib/task-tombstone-commands.mjs +140 -0
  104. package/scripts/postinstall.mjs +14 -0
  105. package/skills/preset-creator/SKILL.md +179 -0
  106. package/skills/preset-creator/references/complex-task-skeleton/README.md +31 -0
  107. package/skills/preset-creator/references/complex-task-skeleton/artifacts/INDEX.md +12 -0
  108. package/skills/preset-creator/references/complex-task-skeleton/brief.md +32 -0
  109. package/skills/preset-creator/references/complex-task-skeleton/execution_strategy.md +71 -0
  110. package/skills/preset-creator/references/complex-task-skeleton/findings.md +24 -0
  111. package/skills/preset-creator/references/complex-task-skeleton/lesson_candidates.md +70 -0
  112. package/skills/preset-creator/references/complex-task-skeleton/long-running-task-contract.md +76 -0
  113. package/skills/preset-creator/references/complex-task-skeleton/progress.md +33 -0
  114. package/skills/preset-creator/references/complex-task-skeleton/references/INDEX.md +13 -0
  115. package/skills/preset-creator/references/complex-task-skeleton/review.md +107 -0
  116. package/skills/preset-creator/references/complex-task-skeleton/task_plan.md +111 -0
  117. package/skills/preset-creator/references/complex-task-skeleton/visual_map.md +50 -0
  118. package/skills/preset-creator/references/preset-package-skeleton.md +296 -0
  119. package/templates/AGENTS.md.template +19 -15
  120. package/templates/dashboard/assets/app-src/00-state.js +1 -0
  121. package/templates/dashboard/assets/app-src/10-router.js +2 -1
  122. package/templates/dashboard/assets/app-src/20-overview.js +11 -5
  123. package/templates/dashboard/assets/app-src/30-tasks.js +92 -246
  124. package/templates/dashboard/assets/app-src/35-task-detail.js +246 -0
  125. package/templates/dashboard/assets/app-src/45-review.js +241 -22
  126. package/templates/dashboard/assets/app-src/50-migration.js +24 -10
  127. package/templates/dashboard/assets/app-src/90-bindings.js +171 -29
  128. package/templates/dashboard/assets/app.css +698 -156
  129. package/templates/dashboard/assets/app.css.manifest.json +9 -0
  130. package/templates/dashboard/assets/app.js +662 -91
  131. package/templates/dashboard/assets/app.manifest.json +1 -0
  132. package/templates/dashboard/assets/css-src/00-foundation.css +342 -0
  133. package/templates/dashboard/assets/css-src/10-panels-flow.css +236 -0
  134. package/templates/dashboard/assets/css-src/20-briefs-controls.css +398 -0
  135. package/templates/dashboard/assets/css-src/30-task-index.css +739 -0
  136. package/templates/dashboard/assets/css-src/35-review-workspace.css +507 -0
  137. package/templates/dashboard/assets/css-src/40-detail-modules-migration.css +427 -0
  138. package/templates/dashboard/assets/css-src/50-responsive-overrides.css +551 -0
  139. package/templates/dashboard/assets/i18n.js +123 -21
  140. package/templates/ledger/Harness-Ledger.md +13 -25
  141. package/templates/lessons/lesson-arch-process-change.md +1 -1
  142. package/templates/lessons/lesson-new-doc.md +1 -1
  143. package/templates/lessons/lesson-ref-change.md +1 -1
  144. package/templates/planning/execution_strategy.md +31 -0
  145. package/templates/planning/lesson_candidates.md +18 -6
  146. package/templates/planning/optional/artifacts/INDEX.md +3 -3
  147. package/templates/planning/optional/references/INDEX.md +3 -3
  148. package/templates/planning/review.md +59 -0
  149. package/templates/planning/task_plan.md +36 -13
  150. package/templates/reference/execution-workflow-standard.md +4 -3
  151. package/templates/reference/pull-request-standard.md +80 -0
  152. package/templates/reference/repo-governance-standard.md +7 -6
  153. package/templates/reference/review-routing-standard.md +6 -0
  154. package/templates/reference/walkthrough-standard.md +2 -1
  155. package/templates/verifier/verifier-output.md +1 -1
  156. package/templates-zh-CN/AGENTS.md.template +20 -16
  157. package/templates-zh-CN/ledger/Harness-Ledger.md +17 -40
  158. package/templates-zh-CN/planning/execution_strategy.md +30 -0
  159. package/templates-zh-CN/planning/lesson_candidates.md +18 -6
  160. package/templates-zh-CN/planning/review.md +59 -1
  161. package/templates-zh-CN/planning/task_plan.md +30 -10
  162. package/templates-zh-CN/reference/adversarial-review-standard.md +1 -1
  163. package/templates-zh-CN/reference/docs-library-standard.md +1 -1
  164. package/templates-zh-CN/reference/execution-workflow-standard.md +4 -3
  165. package/templates-zh-CN/reference/harness-ledger-standard.md +2 -2
  166. package/templates-zh-CN/reference/pull-request-standard.md +106 -0
  167. package/templates-zh-CN/reference/repo-governance-standard.md +4 -3
  168. package/templates-zh-CN/reference/review-routing-standard.md +8 -1
  169. package/templates-zh-CN/reference/walkthrough-standard.md +3 -2
  170. package/templates-zh-CN/walkthrough/Closeout-SSoT.md +1 -1
  171. package/docs-release/assets/dashboard-overview-en.png +0 -0
  172. package/scripts/smoke-dashboard.mjs +0 -92
  173. package/scripts/test-harness.mjs +0 -1395
  174. package/templates/ssot/Feature-SSoT.md +0 -43
  175. package/templates/ssot/Lessons-SSoT.md +0 -44
  176. package/templates-zh-CN/ssot/Feature-SSoT.md +0 -49
  177. package/templates-zh-CN/ssot/Lessons-SSoT.md +0 -49
@@ -18,31 +18,48 @@ import {
18
18
  firstColumn,
19
19
  splitList,
20
20
  splitDependencies,
21
- getColumn,
22
21
  } from "./markdown-utils.mjs";
23
-
24
- export const allowedLessonCandidateTaskStatuses = new Set([
25
- "missing",
26
- "pending-review",
27
- "no-candidate-accepted",
28
- "needs-promotion",
29
- "promoted",
30
- "rejected",
31
- ]);
32
-
33
- export const allowedLessonCandidateRowStatuses = new Set([
34
- "ready-for-review",
35
- "needs-promotion",
36
- "promoted",
37
- "rejected",
38
- ]);
39
-
40
- export const reviewCompleteLessonCandidateStatuses = new Set([
41
- "no-candidate-accepted",
42
- "needs-promotion",
43
- "promoted",
44
- "rejected",
45
- ]);
22
+ import {
23
+ isLessonCandidateDecisionComplete,
24
+ parseLessonCandidateStatus,
25
+ validateLessonCandidateDetailArtifacts,
26
+ } from "./task-lesson-candidates.mjs";
27
+ import {
28
+ assessMaterialsReadiness,
29
+ collectReviewRisks,
30
+ collectStateConflicts,
31
+ deriveLifecycleState,
32
+ deriveReviewQueueState,
33
+ deriveTaskQueues,
34
+ isBlockingReviewRisk,
35
+ parseAgentReviewSubmission,
36
+ parseReviewConfirmation,
37
+ parseTaskIdentity,
38
+ parseTaskTombstone,
39
+ requiresReviewMaterials,
40
+ taskReviewStatus,
41
+ taskScannerVersion,
42
+ } from "./task-review-model.mjs";
43
+ export {
44
+ collectReviewRisks,
45
+ deriveLifecycleState,
46
+ deriveReviewQueueState,
47
+ isBlockingReviewRisk,
48
+ parseAgentReviewSubmission,
49
+ parseReviewConfirmation,
50
+ parseTaskIdentity,
51
+ parseTaskTombstone,
52
+ requiresReviewMaterials,
53
+ taskReviewStatus,
54
+ taskScannerVersion,
55
+ } from "./task-review-model.mjs";
56
+ export {
57
+ allowedLessonCandidateRowStatuses,
58
+ allowedLessonCandidateTaskStatuses,
59
+ isLessonCandidateDecisionComplete,
60
+ parseLessonCandidateStatus,
61
+ reviewCompleteLessonCandidateStatuses,
62
+ } from "./task-lesson-candidates.mjs";
46
63
 
47
64
  export function parseTaskState(progressContent) {
48
65
  return parseTaskStateInfo(progressContent).state;
@@ -307,7 +324,11 @@ export function collectTasks(target) {
307
324
  const visualMap = readVisualMapContractFile(taskDir, taskPlan);
308
325
  const progress = readFileSafe(progressPath);
309
326
  const review = readFileSafe(reviewPath);
310
- const lessonCandidates = parseLessonCandidateStatus(readFileSafe(lessonCandidatesPath));
327
+ const parsedLessonCandidates = parseLessonCandidateStatus(readFileSafe(lessonCandidatesPath));
328
+ const lessonDetailIssues = validateLessonCandidateDetailArtifacts(target, taskDir, parsedLessonCandidates);
329
+ const lessonCandidates = lessonDetailIssues.length
330
+ ? { ...parsedLessonCandidates, issues: [...parsedLessonCandidates.issues, ...lessonDetailIssues] }
331
+ : parsedLessonCandidates;
311
332
  const phases = parsePhases(visualMap.content);
312
333
  const completion =
313
334
  phases.length > 0
@@ -318,6 +339,8 @@ export function collectTasks(target) {
318
339
  : 0;
319
340
  const relative = toPosix(path.relative(target.projectRoot, taskDir));
320
341
  const id = taskIdForDirectory(target, taskDir);
342
+ const identity = parseTaskIdentity(taskPlan, id);
343
+ const tombstone = parseTaskTombstone(taskPlan);
321
344
  const title = titleFromMarkdown(brief.content || taskPlan, path.basename(taskDir));
322
345
  const stateInfo = parseTaskStateInfo(progress);
323
346
  const budget = parseTaskBudget(taskPlan);
@@ -329,13 +352,70 @@ export function collectTasks(target) {
329
352
  const briefVisualStatus = explicitVisualMapStatus(brief.content);
330
353
  const visualMapStatus = briefVisualStatus === "not-needed" && visualMap.status === "missing" ? "not-needed" : visualMap.status;
331
354
  const risks = collectReviewRisks(review);
332
- const reviewConfirmation = parseReviewConfirmation(review);
333
- const reviewStatus = taskReviewStatus({ reviewContent: review, risks, confirmation: reviewConfirmation });
355
+ const reviewSubmission = parseAgentReviewSubmission(review, { taskKey: identity.taskKey });
356
+ const reviewConfirmation = parseReviewConfirmation(review, {
357
+ taskKey: identity.taskKey,
358
+ projectRoot: target.projectRoot,
359
+ taskDir,
360
+ reviewPath,
361
+ progressPath,
362
+ });
363
+ const reviewStatus = taskReviewStatus({ reviewContent: review, risks, confirmation: reviewConfirmation, submission: reviewSubmission });
334
364
  const closeoutInfo = taskCloseoutInfo(target, taskPlanPath);
335
365
  const lifecycleState = deriveLifecycleState({ state: stateInfo.state, reviewStatus, closeoutStatus: closeoutInfo.status });
366
+ const materialReadiness = assessMaterialsReadiness({
367
+ budget,
368
+ taskDir,
369
+ taskPlan,
370
+ brief,
371
+ visualMap,
372
+ reviewSubmission,
373
+ lessonCandidates,
374
+ phases,
375
+ longRunningContractPath,
376
+ reviewSurfaceRequired: requiresReviewMaterials({
377
+ state: stateInfo.state,
378
+ lifecycleState,
379
+ closeoutStatus: closeoutInfo.status,
380
+ }),
381
+ });
336
382
  const stateConflicts = collectStateConflicts({ state: stateInfo.state, reviewStatus, closeoutStatus: closeoutInfo.status, lifecycleState });
383
+ const reviewQueueState = deriveReviewQueueState({
384
+ state: stateInfo.state,
385
+ lifecycleState,
386
+ reviewStatus,
387
+ closeoutStatus: closeoutInfo.status,
388
+ budget,
389
+ walkthroughPath: closeoutInfo.walkthroughPath,
390
+ lessonCandidateDecisionComplete: isLessonCandidateDecisionComplete(lessonCandidates),
391
+ materialsReady: materialReadiness.ready,
392
+ deletionState: tombstone.deletionState,
393
+ });
394
+ const queueModel = deriveTaskQueues({
395
+ id,
396
+ title,
397
+ state: stateInfo.state,
398
+ budget,
399
+ reviewStatus,
400
+ reviewSubmission,
401
+ reviewConfirmation,
402
+ reviewQueueState,
403
+ materialIssues: materialReadiness.issues,
404
+ risks,
405
+ stateConflicts,
406
+ lessonCandidates,
407
+ closeoutStatus: closeoutInfo.status,
408
+ tombstone,
409
+ taskDir,
410
+ target,
411
+ });
337
412
  return {
338
413
  id,
414
+ taskKey: identity.taskKey,
415
+ currentPath: `TARGET:${relative}`,
416
+ originalPath: `TARGET:${relative}`,
417
+ aliases: [],
418
+ identitySource: identity.identitySource,
339
419
  shortId: path.basename(taskDir),
340
420
  title,
341
421
  path: `TARGET:${relative}`,
@@ -368,11 +448,19 @@ export function collectTasks(target) {
368
448
  presetVersion: metadata.presetVersion,
369
449
  migrationTargetLevel: metadata.migrationTargetLevel,
370
450
  migrationAchievedLevel: metadata.migrationAchievedLevel,
371
- evidenceBundle: metadata.evidenceBundle,
451
+ evidenceBundle: formatEvidenceBundle(metadata.evidenceBundle),
372
452
  migrationSnapshot: collectMigrationSnapshot(target, metadata),
373
453
  lifecycleState,
374
454
  reviewStatus,
455
+ reviewSubmitted: Boolean(reviewSubmission?.submitted),
456
+ reviewSubmission,
457
+ reviewQueueState,
375
458
  reviewConfirmation,
459
+ materialsReady: materialReadiness.ready,
460
+ materialIssues: materialReadiness.issues,
461
+ taskQueues: queueModel.taskQueues,
462
+ queueReasons: queueModel.queueReasons,
463
+ repairPrompt: queueModel.repairPrompt,
376
464
  closeoutStatus: closeoutInfo.status,
377
465
  walkthroughPath: closeoutInfo.walkthroughPath ? `TARGET:${closeoutInfo.walkthroughPath}` : "",
378
466
  lessonCandidatePath: fs.existsSync(lessonCandidatesPath)
@@ -383,6 +471,7 @@ export function collectTasks(target) {
383
471
  lessonCandidatePromotionState: lessonCandidates.promotionState,
384
472
  lessonCandidateCloseoutToken: lessonCandidates.closeoutToken,
385
473
  lessonCandidateRowCount: lessonCandidates.rows.length,
474
+ lessonCandidateRows: lessonCandidates.rows,
386
475
  lessonCandidateOpenCount: lessonCandidates.openCount,
387
476
  lessonCandidateIssues: lessonCandidates.issues,
388
477
  lessonCandidateDecisionComplete: isLessonCandidateDecisionComplete(lessonCandidates),
@@ -390,6 +479,16 @@ export function collectTasks(target) {
390
479
  ? `TARGET:${toPosix(path.relative(target.projectRoot, longRunningContractPath))}`
391
480
  : "",
392
481
  longRunningContractStatus: fs.existsSync(longRunningContractPath) ? "present" : "missing",
482
+ deletionState: tombstone.deletionState,
483
+ supersededBy: tombstone.supersededBy,
484
+ supersedes: tombstone.supersedes,
485
+ deleteReason: tombstone.deleteReason,
486
+ hiddenByDefault: tombstone.hiddenByDefault,
487
+ reopenEligible: tombstone.reopenEligible,
488
+ archiveEligible: tombstone.archiveEligible,
489
+ tombstoneSourcePath: tombstone.tombstoneSourcePath
490
+ ? `TARGET:${toPosix(path.relative(target.projectRoot, path.join(taskDir, "task_plan.md")))}#Task Tombstone`
491
+ : "",
393
492
  stateConflicts,
394
493
  completion,
395
494
  phases,
@@ -432,136 +531,11 @@ function collectMigrationSnapshot(target, metadata) {
432
531
  };
433
532
  }
434
533
 
435
- export function parseLessonCandidateStatus(content) {
436
- const text = String(content || "");
437
- if (!text.trim()) {
438
- return emptyLessonCandidateStatus("missing", ["missing-candidate-file"]);
439
- }
440
-
441
- const fields = lessonCandidateFields(text);
442
- const declaredStatus = normalizeLessonCandidateStatus(fields.get("task-level status") || "pending-review");
443
- const reviewDecision = normalizeCandidateField(fields.get("review decision") || "pending-human-review");
444
- const promotionState = normalizeCandidateField(fields.get("promotion state") || "not-promoted");
445
- const closeoutToken = String(fields.get("closeout token") || "pending").trim();
446
- const rows = lessonCandidateRows(text);
447
- const issues = [];
448
-
449
- if (!allowedLessonCandidateTaskStatuses.has(declaredStatus)) {
450
- issues.push(`invalid-task-status:${declaredStatus}`);
451
- }
452
- for (const row of rows) {
453
- if (!allowedLessonCandidateRowStatuses.has(row.status)) issues.push(`invalid-row-status:${row.id || "missing-id"}:${row.status}`);
454
- }
455
-
456
- const aggregateStatus = aggregateLessonCandidateStatus(rows, declaredStatus);
457
- if (declaredStatus !== aggregateStatus && declaredStatus !== "missing") {
458
- issues.push(`status-aggregate-mismatch:${declaredStatus}->${aggregateStatus}`);
459
- }
460
- if (aggregateStatus === "no-candidate-accepted" && !noCandidateReason(text)) {
461
- issues.push("missing-no-candidate-reason");
462
- }
463
-
464
- return {
465
- status: aggregateStatus,
466
- declaredStatus,
467
- schemaVersion: fields.get("schema version") || "",
468
- reviewDecision,
469
- promotionState,
470
- closeoutToken,
471
- rows,
472
- openCount: rows.filter((row) => ["ready-for-review", "needs-promotion"].includes(row.status)).length,
473
- issues,
474
- };
475
- }
476
-
477
- export function isLessonCandidateDecisionComplete(candidateStatus) {
478
- if (!candidateStatus || candidateStatus.issues?.length) return false;
479
- return reviewCompleteLessonCandidateStatuses.has(candidateStatus.status);
534
+ function formatEvidenceBundle(value) {
535
+ const normalized = String(value || "").replace(/^TARGET:/, "").replace(/^\/+/, "");
536
+ return normalized ? `TARGET:${normalized}` : "";
480
537
  }
481
538
 
482
- function emptyLessonCandidateStatus(status, issues = []) {
483
- return {
484
- status,
485
- declaredStatus: status,
486
- schemaVersion: "",
487
- reviewDecision: "",
488
- promotionState: "",
489
- closeoutToken: "",
490
- rows: [],
491
- openCount: 0,
492
- issues,
493
- };
494
- }
495
-
496
- function lessonCandidateFields(content) {
497
- const { header, rows } = tableAfterHeading(content, /^Field$/i);
498
- const fieldIndex = firstColumn(header, ["Field", "字段"]);
499
- const valueIndex = firstColumn(header, ["Value", "值"]);
500
- const fields = new Map();
501
- if (fieldIndex < 0 || valueIndex < 0) return fields;
502
- for (const row of rows) {
503
- const key = String(row[fieldIndex] || "").trim().toLowerCase();
504
- if (key) fields.set(key, String(row[valueIndex] || "").trim());
505
- }
506
- return fields;
507
- }
508
-
509
- function lessonCandidateRows(content) {
510
- const { header, rows } = tableAfterHeading(content, /^ID$/i);
511
- const idIndex = firstColumn(header, ["ID", "候选 ID"]);
512
- const statusIndex = firstColumn(header, ["Row Status", "行状态", "Status", "状态"]);
513
- const titleIndex = firstColumn(header, ["Title", "标题"]);
514
- const decisionIndex = firstColumn(header, ["Review Decision", "审查决定"]);
515
- const targetIndex = firstColumn(header, ["Promotion Target", "沉淀目标"]);
516
- if (idIndex < 0 || statusIndex < 0) return [];
517
- return rows
518
- .filter((row) => /^LC-[A-Za-z0-9-]+$/i.test(row[idIndex] || ""))
519
- .map((row) => ({
520
- id: row[idIndex] || "",
521
- status: normalizeLessonCandidateStatus(row[statusIndex] || ""),
522
- title: row[titleIndex] || "",
523
- reviewDecision: row[decisionIndex] || "",
524
- promotionTarget: row[targetIndex] || "",
525
- }));
526
- }
527
-
528
- function normalizeLessonCandidateStatus(value) {
529
- return String(value || "")
530
- .replace(/`/g, "")
531
- .trim()
532
- .toLowerCase()
533
- .replaceAll("_", "-")
534
- .replace(/\s+/g, "-");
535
- }
536
-
537
- function normalizeCandidateField(value) {
538
- return String(value || "").replace(/`/g, "").trim().toLowerCase().replaceAll("_", "-").replace(/\s+/g, "-");
539
- }
540
-
541
- function aggregateLessonCandidateStatus(rows, declaredStatus) {
542
- if (rows.length === 0) return declaredStatus === "no-candidate-accepted" ? "no-candidate-accepted" : declaredStatus;
543
- const statuses = rows.map((row) => row.status);
544
- if (statuses.includes("ready-for-review")) return "pending-review";
545
- if (statuses.includes("needs-promotion")) return "needs-promotion";
546
- if (statuses.every((status) => status === "promoted")) return "promoted";
547
- if (statuses.every((status) => status === "rejected")) return "rejected";
548
- if (statuses.every((status) => ["promoted", "rejected"].includes(status))) return "promoted";
549
- return declaredStatus;
550
- }
551
-
552
- function noCandidateReason(content) {
553
- const lines = String(content || "").split(/\r?\n/);
554
- const start = lines.findIndex((line) => /^##\s*No-Candidate Reason\s*$/i.test(line.trim()));
555
- if (start < 0) return "";
556
- const body = [];
557
- for (const line of lines.slice(start + 1)) {
558
- if (/^##\s+/.test(line)) break;
559
- body.push(line);
560
- }
561
- return body.join("\n").replace(/`/g, "").trim();
562
- }
563
-
564
-
565
539
  function taskCloseoutInfo(target, taskPlanPath) {
566
540
  const closeout = readFileSafe(path.join(target.docsRoot, "10-WALKTHROUGH/Closeout-SSoT.md"));
567
541
  if (!closeout.trim()) return { status: "missing", walkthroughPath: "" };
@@ -585,91 +559,11 @@ function extractWalkthroughPath(target, closeoutLine) {
585
559
  return projectRelative;
586
560
  }
587
561
 
588
- export function parseReviewConfirmation(reviewContent) {
589
- const match = String(reviewContent || "").match(/^##\s*(?:Human Review Confirmation|人工审查确认)\s*$([\s\S]*?)(?=^##\s+|\s*$)/im);
590
- if (!match) return null;
591
- const block = match[1] || "";
592
- const timeMatch = block.match(/\|\s*(\d{4}-\d{2}-\d{2}[^|]*)\|/);
593
- const reviewerMatch = block.match(/Reviewer\s*[::]\s*([^\n]+)/i) || block.match(/审查人\s*[::]\s*([^\n]+)/);
594
- return {
595
- confirmed: true,
596
- confirmedAt: timeMatch ? timeMatch[1].trim() : "",
597
- reviewer: reviewerMatch ? reviewerMatch[1].trim() : "",
598
- };
599
- }
600
-
601
- export function taskReviewStatus({ reviewContent = "", risks = [], confirmation = null } = {}) {
602
- if (risks.some(isBlockingReviewRisk)) return "blocked-open-findings";
603
- if (confirmation?.confirmed) return "confirmed";
604
- if (!String(reviewContent || "").trim()) return "missing";
605
- if (/Verdict\s*[::]\s*yes/i.test(reviewContent) || /本轮已检查|未发现阻塞目标的重要发现/.test(reviewContent)) return "reviewed-unconfirmed";
606
- return "required";
607
- }
608
-
609
- export function isBlockingReviewRisk(risk) {
610
- return /^P[0-2]$/i.test(risk?.severity || "") && (risk.open || risk.blocksRelease);
611
- }
612
-
613
- export function deriveLifecycleState({ state = "unknown", reviewStatus = "missing", closeoutStatus = "missing" } = {}) {
614
- if (closeoutStatus === "closed") return "closed";
615
- if (state === "blocked") return "blocked";
616
- if (reviewStatus === "blocked-open-findings") return "review-blocked";
617
- if (state === "done") return "closing";
618
- if (state === "review") return "in_review";
619
- if (state === "in_progress") return "active";
620
- if (["planned", "not_started"].includes(state)) return "ready";
621
- return "unknown";
622
- }
623
-
624
- function collectStateConflicts({ state, reviewStatus, closeoutStatus, lifecycleState }) {
625
- const conflicts = [];
626
- if (state === "done" && closeoutStatus !== "closed") {
627
- conflicts.push({
628
- code: "done-without-closeout",
629
- severity: "warn",
630
- message: "Task state is done, but closeout is still missing or pending.",
631
- });
632
- }
633
- if (reviewStatus === "blocked-open-findings") {
634
- conflicts.push({
635
- code: "review-blocked-open-findings",
636
- severity: "block",
637
- message: "Open P0-P2 review findings block human review confirmation.",
638
- });
639
- }
640
- if (lifecycleState === "closed" && reviewStatus === "blocked-open-findings") {
641
- conflicts.push({
642
- code: "closed-with-blocking-review",
643
- severity: "block",
644
- message: "Closeout is closed while review findings still block release.",
645
- });
646
- }
647
- return conflicts;
648
- }
649
-
650
562
  function collectHandoffs(progressContent, taskId) {
651
563
  if (!/Coordinator Handoff/i.test(progressContent) || !/pending-coordinator-pass/i.test(progressContent)) return [];
652
564
  return [{ id: `H-${taskId}`, from: "worker", to: "coordinator", state: "pending", summary: "Coordinator handoff pending" }];
653
565
  }
654
566
 
655
- export function collectReviewRisks(reviewContent) {
656
- const { header, rows } = tableAfterHeading(reviewContent, /^ID$/i);
657
- const severityIndex = getColumn(header, "Severity");
658
- const findingIndex = getColumn(header, "Finding");
659
- const openIndex = getColumn(header, "Open");
660
- const blocksIndex = getColumn(header, "Blocks Release");
661
- if (severityIndex < 0 || findingIndex < 0) return [];
662
- return rows
663
- .filter((row) => /^P[0-3]$/i.test(row[severityIndex] || ""))
664
- .map((row) => ({
665
- id: row[0],
666
- severity: row[severityIndex],
667
- open: /^yes$/i.test(row[openIndex] || "no"),
668
- blocksRelease: /^yes$/i.test(row[blocksIndex] || "no"),
669
- summary: row[findingIndex],
670
- }));
671
- }
672
-
673
567
  function collectEvidence(progressContent) {
674
568
  const matches = [...progressContent.matchAll(/\b(command|diff|fixture|screenshot|review|report):((?:PUBLIC|PRIVATE|TARGET|EXTERNAL|URL):[^:\s|]+):([^\n|]+)/g)];
675
569
  return matches.map((match, index) => ({
@@ -0,0 +1,140 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import {
4
+ normalizeTarget,
5
+ nowTimestamp,
6
+ readFileSafe,
7
+ toPosix,
8
+ datePrefix,
9
+ } from "./core-shared.mjs";
10
+ import { collectTasks } from "./task-scanner.mjs";
11
+ import {
12
+ beginGovernanceSync,
13
+ commitGovernanceSync,
14
+ releaseGovernanceSync,
15
+ } from "./governance-sync.mjs";
16
+
17
+ export function supersedeTask(targetInput, oldRef, { by = "", reason = "" } = {}) {
18
+ if (!by) throw new Error("task-supersede requires --by <new-task-id>");
19
+ const target = normalizeTarget(targetInput);
20
+ const oldTask = resolveTask(target, oldRef);
21
+ const newTask = resolveTask(target, by);
22
+ const governanceContext = beginGovernanceSync(target, { operation: `task-supersede ${oldTask.id}` });
23
+ try {
24
+ writeTombstone(target, oldTask, {
25
+ State: "superseded",
26
+ "Superseded By": newTask.id,
27
+ Reason: reason || "superseded",
28
+ Operator: "coordinator",
29
+ Timestamp: nowTimestamp(),
30
+ "Reopen Eligible": "yes",
31
+ "Archive Eligible": "no",
32
+ });
33
+ appendProgress(target, oldTask, `task-supersede: superseded by ${newTask.id}`, reason || "superseded");
34
+ appendSupersedes(target, newTask, oldTask.id);
35
+ const commit = commitGovernanceSync(contextFor(target, governanceContext), taskPaths(target, oldTask, newTask), {
36
+ message: `chore(harness): supersede task ${oldTask.id}`,
37
+ });
38
+ return { taskId: oldTask.id, supersededBy: newTask.id, reason: reason || "superseded", governance: { commit } };
39
+ } finally {
40
+ releaseGovernanceSync(governanceContext);
41
+ }
42
+ }
43
+
44
+ export function softDeleteTask(targetInput, taskRef, { reason = "" } = {}) {
45
+ const target = normalizeTarget(targetInput);
46
+ const task = resolveTask(target, taskRef);
47
+ return writeDeletionState(target, task, "soft-deleted", reason || "soft-delete", "task-delete --soft");
48
+ }
49
+
50
+ export function archiveTask(targetInput, taskRef, { reason = "" } = {}) {
51
+ const target = normalizeTarget(targetInput);
52
+ const task = resolveTask(target, taskRef);
53
+ return writeDeletionState(target, task, "archived", reason || "archive", "task-archive");
54
+ }
55
+
56
+ export function reopenTask(targetInput, taskRef, { reason = "" } = {}) {
57
+ const target = normalizeTarget(targetInput);
58
+ const task = resolveTask(target, taskRef);
59
+ const governanceContext = beginGovernanceSync(target, { operation: `task-reopen ${task.id}` });
60
+ try {
61
+ const taskPlanPath = path.join(target.projectRoot, task.taskPlanPath.replace(/^TARGET:/, ""));
62
+ const content = readFileSafe(taskPlanPath);
63
+ const next = content.replace(/\n##\s*(?:Task Tombstone|任务墓碑)\s*$[\s\S]*?(?=^##\s+|(?![\s\S]))/im, "");
64
+ fs.writeFileSync(taskPlanPath, next.endsWith("\n") ? next : `${next}\n`);
65
+ appendProgress(target, task, "task-reopen", reason || "reopened");
66
+ const commit = commitGovernanceSync(governanceContext, taskPaths(target, task), {
67
+ message: `chore(harness): reopen task ${task.id}`,
68
+ });
69
+ return { taskId: task.id, deletionState: "active", reason: reason || "reopened", governance: { commit } };
70
+ } finally {
71
+ releaseGovernanceSync(governanceContext);
72
+ }
73
+ }
74
+
75
+ function writeDeletionState(target, task, deletionState, reason, action) {
76
+ const governanceContext = beginGovernanceSync(target, { operation: `${action} ${task.id}` });
77
+ try {
78
+ writeTombstone(target, task, {
79
+ State: deletionState,
80
+ Reason: reason,
81
+ Operator: "coordinator",
82
+ Timestamp: nowTimestamp(),
83
+ "Reopen Eligible": "yes",
84
+ "Archive Eligible": deletionState === "archived" ? "yes" : "no",
85
+ });
86
+ appendProgress(target, task, action, reason);
87
+ const commit = commitGovernanceSync(governanceContext, taskPaths(target, task), {
88
+ message: `chore(harness): ${action.replace(/\s+/g, " ")} ${task.id}`,
89
+ });
90
+ return { taskId: task.id, deletionState, reason, governance: { commit } };
91
+ } finally {
92
+ releaseGovernanceSync(governanceContext);
93
+ }
94
+ }
95
+
96
+ function taskPaths(target, ...tasks) {
97
+ return [...new Set(tasks.flatMap((task) => [task.taskPlanPath, task.progressPath]).filter(Boolean).map((item) => toPosix(item.replace(/^TARGET:/, ""))))];
98
+ }
99
+
100
+ function contextFor(_target, context) {
101
+ return context;
102
+ }
103
+
104
+ function resolveTask(target, ref) {
105
+ const normalized = String(ref || "").trim();
106
+ const matches = collectTasks(target).filter((task) => {
107
+ const bare = datePrefix.test(task.shortId) ? task.shortId.replace(datePrefix, "") : task.shortId;
108
+ return task.id === normalized || task.shortId === normalized || task.id.endsWith(`/${normalized}`) || bare === normalized;
109
+ });
110
+ if (matches.length === 1) return matches[0];
111
+ if (matches.length > 1) throw new Error(`Ambiguous task reference: ${ref}`);
112
+ throw new Error(`Task not found: ${ref}`);
113
+ }
114
+
115
+ function writeTombstone(target, task, fields) {
116
+ const taskPlanPath = path.join(target.projectRoot, task.taskPlanPath.replace(/^TARGET:/, ""));
117
+ const content = readFileSafe(taskPlanPath).replace(/\n##\s*(?:Task Tombstone|任务墓碑)\s*$[\s\S]*?(?=^##\s+|(?![\s\S]))/im, "");
118
+ const block = ["", "## Task Tombstone", "", "| Field | Value |", "| --- | --- |", ...Object.entries(fields).map(([key, value]) => `| ${key} | ${escapeCell(value)} |`), ""].join("\n");
119
+ fs.writeFileSync(taskPlanPath, `${content.trimEnd()}\n${block}`);
120
+ }
121
+
122
+ function appendSupersedes(target, task, oldId) {
123
+ const taskPlanPath = path.join(target.projectRoot, task.taskPlanPath.replace(/^TARGET:/, ""));
124
+ const content = readFileSafe(taskPlanPath);
125
+ if (/^Supersedes\s*[::]/im.test(content)) {
126
+ fs.writeFileSync(taskPlanPath, content.replace(/^Supersedes\s*[::]\s*(.*)$/im, (_m, current) => `Supersedes: ${[current, oldId].filter(Boolean).join(", ")}`));
127
+ return;
128
+ }
129
+ fs.writeFileSync(taskPlanPath, `${content.trimEnd()}\nSupersedes: ${oldId}\n`);
130
+ }
131
+
132
+ function appendProgress(target, task, action, reason) {
133
+ const progressPath = path.join(target.projectRoot, task.progressPath.replace(/^TARGET:/, ""));
134
+ const relative = toPosix(path.relative(target.projectRoot, progressPath));
135
+ fs.appendFileSync(progressPath, `\n\n## Tombstone Log\n\n- ${nowTimestamp()} ${action}: ${escapeCell(reason)} (${relative})\n`);
136
+ }
137
+
138
+ function escapeCell(value) {
139
+ return String(value || "").replace(/\r?\n/g, " ").replaceAll("|", "\\|").trim();
140
+ }
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { seedBundledPresets } from "./lib/harness-core.mjs";
4
+
5
+ if (process.env.CODING_AGENT_HARNESS_SKIP_POSTINSTALL === "1") process.exit(0);
6
+
7
+ try {
8
+ const result = seedBundledPresets({ scope: "user" });
9
+ const changed = result.created + result.overwritten;
10
+ const summary = changed > 0 ? `${changed} bundled presets installed` : `${result.skipped} bundled presets already present`;
11
+ console.log(`coding-agent-harness postinstall: ${summary} at ${result.target}`);
12
+ } catch (error) {
13
+ console.warn(`coding-agent-harness postinstall: preset seed skipped (${error.message})`);
14
+ }