cc-devflow 4.5.2 → 4.5.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 (100) hide show
  1. package/.claude/skills/cc-act/CHANGELOG.md +19 -0
  2. package/.claude/skills/cc-act/PLAYBOOK.md +14 -1
  3. package/.claude/skills/cc-act/SKILL.md +46 -6
  4. package/.claude/skills/cc-act/assets/PR_BRIEF_TEMPLATE.md +44 -1
  5. package/.claude/skills/cc-act/assets/RELEASE_NOTE_TEMPLATE.md +18 -1
  6. package/.claude/skills/cc-act/references/closure-contract.md +3 -0
  7. package/.claude/skills/cc-act/scripts/cc-act-common.sh +27 -1
  8. package/.claude/skills/cc-act/scripts/render-pr-brief.sh +31 -0
  9. package/.claude/skills/cc-act/scripts/verify-act-gate.sh +6 -0
  10. package/.claude/skills/cc-check/CHANGELOG.md +18 -0
  11. package/.claude/skills/cc-check/PLAYBOOK.md +38 -7
  12. package/.claude/skills/cc-check/SKILL.md +39 -7
  13. package/.claude/skills/cc-check/assets/REPORT_CARD_TEMPLATE.json +61 -0
  14. package/.claude/skills/cc-check/references/gate-contract.md +11 -0
  15. package/.claude/skills/cc-check/references/review-contract.md +17 -1
  16. package/.claude/skills/cc-check/scripts/render-report-card.js +37 -0
  17. package/.claude/skills/cc-check/scripts/verify-gate.sh +7 -0
  18. package/.claude/skills/cc-do/CHANGELOG.md +18 -0
  19. package/.claude/skills/cc-do/PLAYBOOK.md +20 -13
  20. package/.claude/skills/cc-do/SKILL.md +37 -17
  21. package/.claude/skills/cc-do/references/execution-recovery.md +19 -5
  22. package/.claude/skills/cc-do/references/parallel-dispatch.md +6 -4
  23. package/.claude/skills/cc-do/scripts/detect-file-conflicts.sh +49 -3
  24. package/.claude/skills/cc-do/scripts/verify-task-gates.sh +19 -6
  25. package/.claude/skills/cc-do/scripts/write-task-checkpoint.sh +14 -2
  26. package/.claude/skills/cc-investigate/CHANGELOG.md +24 -0
  27. package/.claude/skills/cc-investigate/PLAYBOOK.md +35 -13
  28. package/.claude/skills/cc-investigate/SKILL.md +87 -20
  29. package/.claude/skills/cc-investigate/assets/ANALYSIS_TEMPLATE.md +68 -3
  30. package/.claude/skills/cc-investigate/assets/TASKS_TEMPLATE.md +9 -4
  31. package/.claude/skills/cc-investigate/assets/TASK_MANIFEST_TEMPLATE.json +41 -2
  32. package/.claude/skills/cc-investigate/references/investigation-contract.md +46 -0
  33. package/.claude/skills/cc-plan/CHANGELOG.md +32 -0
  34. package/.claude/skills/cc-plan/PLAYBOOK.md +26 -8
  35. package/.claude/skills/cc-plan/SKILL.md +79 -34
  36. package/.claude/skills/cc-plan/assets/DESIGN_TEMPLATE.md +71 -3
  37. package/.claude/skills/cc-plan/assets/TASKS_TEMPLATE.md +32 -0
  38. package/.claude/skills/cc-plan/assets/TASK_MANIFEST_TEMPLATE.json +76 -2
  39. package/.claude/skills/cc-plan/assets/TINY_DESIGN_TEMPLATE.md +58 -0
  40. package/.claude/skills/cc-plan/references/planning-contract.md +26 -4
  41. package/.claude/skills/cc-roadmap/CHANGELOG.md +14 -0
  42. package/.claude/skills/cc-roadmap/PLAYBOOK.md +10 -7
  43. package/.claude/skills/cc-roadmap/SKILL.md +43 -23
  44. package/.claude/skills/cc-roadmap/assets/BACKLOG_TEMPLATE.md +10 -0
  45. package/.claude/skills/cc-roadmap/assets/ROADMAP_TEMPLATE.md +15 -0
  46. package/.claude/skills/cc-roadmap/assets/TRACKING_TEMPLATE.json +1 -1
  47. package/.claude/skills/cc-roadmap/references/roadmap-dialogue.md +11 -7
  48. package/.claude/skills/cc-simplify/CHANGELOG.md +6 -0
  49. package/.claude/skills/cc-simplify/SKILL.md +10 -1
  50. package/.claude/skills/cc-spec-init/CHANGELOG.md +6 -0
  51. package/.claude/skills/cc-spec-init/SKILL.md +14 -1
  52. package/CHANGELOG.md +29 -0
  53. package/README.md +10 -2
  54. package/README.zh-CN.md +10 -2
  55. package/bin/cc-devflow-cli.js +93 -2
  56. package/docs/examples/example-bindings.json +7 -7
  57. package/docs/examples/full-design-blocked/BACKLOG.md +1 -1
  58. package/docs/examples/full-design-blocked/README.md +1 -1
  59. package/docs/examples/full-design-blocked/ROADMAP.md +1 -1
  60. package/docs/examples/full-design-blocked/changes/REQ-002-bulk-invite-import/planning/design.md +1 -1
  61. package/docs/examples/full-design-blocked/changes/REQ-002-bulk-invite-import/planning/tasks.md +1 -1
  62. package/docs/examples/full-design-blocked/roadmap-tracking.json +1 -1
  63. package/docs/examples/local-handoff/BACKLOG.md +1 -1
  64. package/docs/examples/local-handoff/README.md +1 -1
  65. package/docs/examples/local-handoff/ROADMAP.md +1 -1
  66. package/docs/examples/local-handoff/changes/REQ-003-audit-log-export/planning/design.md +1 -1
  67. package/docs/examples/local-handoff/changes/REQ-003-audit-log-export/planning/tasks.md +1 -1
  68. package/docs/examples/local-handoff/roadmap-tracking.json +1 -1
  69. package/docs/examples/pdca-loop/BACKLOG.md +1 -1
  70. package/docs/examples/pdca-loop/README.md +1 -1
  71. package/docs/examples/pdca-loop/ROADMAP.md +1 -1
  72. package/docs/examples/pdca-loop/changes/REQ-001-copy-invite-link/planning/design.md +1 -1
  73. package/docs/examples/pdca-loop/changes/REQ-001-copy-invite-link/planning/task-manifest.json +2 -2
  74. package/docs/examples/pdca-loop/changes/REQ-001-copy-invite-link/planning/tasks.md +1 -1
  75. package/docs/examples/pdca-loop/roadmap-tracking.json +1 -1
  76. package/docs/get-shit-done-strategy-audit.md +518 -0
  77. package/docs/skill-strategy-audit.md +48 -0
  78. package/lib/compiler/__tests__/inventory.test.js +51 -0
  79. package/lib/compiler/inventory.js +78 -0
  80. package/lib/skill-runtime/__tests__/approve.test.js +92 -0
  81. package/lib/skill-runtime/__tests__/autopilot.test.js +4 -0
  82. package/lib/skill-runtime/__tests__/planner.tdd.test.js +20 -0
  83. package/lib/skill-runtime/__tests__/query.test.js +147 -1
  84. package/lib/skill-runtime/__tests__/readiness.test.js +53 -0
  85. package/lib/skill-runtime/__tests__/release.test.js +85 -0
  86. package/lib/skill-runtime/__tests__/runtime.integration.test.js +30 -1
  87. package/lib/skill-runtime/__tests__/schemas.test.js +56 -0
  88. package/lib/skill-runtime/__tests__/worker-run.test.js +29 -0
  89. package/lib/skill-runtime/errors.js +39 -0
  90. package/lib/skill-runtime/index.js +8 -0
  91. package/lib/skill-runtime/operations/approve.js +17 -2
  92. package/lib/skill-runtime/operations/release.js +6 -3
  93. package/lib/skill-runtime/operations/worker-run.js +30 -0
  94. package/lib/skill-runtime/planner.js +10 -2
  95. package/lib/skill-runtime/query-registry.js +101 -0
  96. package/lib/skill-runtime/query.js +159 -91
  97. package/lib/skill-runtime/readiness.js +84 -0
  98. package/lib/skill-runtime/schemas.js +39 -4
  99. package/lib/skill-runtime/trace.js +22 -0
  100. package/package.json +1 -1
@@ -0,0 +1,39 @@
1
+ /**
2
+ * [INPUT]: 接收 runtime/query/compiler 边界抛出的错误或失败字段。
3
+ * [OUTPUT]: 生成可序列化 named error,保留 artifact refs 与 rescue action。
4
+ * [POS]: skill runtime 的失败语义层,避免用 null/false/string 表达可恢复失败。
5
+ * [PROTOCOL]: 变更时更新此头部,然后检查 CLAUDE.md
6
+ */
7
+
8
+ class SkillRuntimeError extends Error {
9
+ constructor(name, message, options = {}) {
10
+ super(message);
11
+ this.name = name;
12
+ this.artifactRefs = options.artifactRefs || [];
13
+ this.rescueAction = options.rescueAction || 'inspect-runtime-artifacts';
14
+ this.details = options.details || {};
15
+ }
16
+ }
17
+
18
+ function namedError(name, message, options = {}) {
19
+ return new SkillRuntimeError(name, message, options);
20
+ }
21
+
22
+ function serializeError(error, fallbackName = 'SkillRuntimeError') {
23
+ const name = error?.name || fallbackName;
24
+ const message = error?.message || String(error || 'Unknown runtime error');
25
+
26
+ return {
27
+ name,
28
+ message,
29
+ artifactRefs: error?.artifactRefs || [],
30
+ rescueAction: error?.rescueAction || 'inspect-runtime-artifacts',
31
+ details: error?.details || {}
32
+ };
33
+ }
34
+
35
+ module.exports = {
36
+ SkillRuntimeError,
37
+ namedError,
38
+ serializeError
39
+ };
@@ -9,6 +9,10 @@ const store = require('./store');
9
9
  const schemas = require('./schemas');
10
10
  const planner = require('./planner');
11
11
  const query = require('./query');
12
+ const queryRegistry = require('./query-registry');
13
+ const errors = require('./errors');
14
+ const trace = require('./trace');
15
+ const readiness = require('./readiness');
12
16
  const intent = require('./intent');
13
17
  const artifacts = require('./artifacts');
14
18
  const lifecycle = require('./lifecycle');
@@ -24,6 +28,10 @@ module.exports = {
24
28
  ...schemas,
25
29
  ...planner,
26
30
  ...query,
31
+ ...queryRegistry,
32
+ ...errors,
33
+ ...trace,
34
+ ...readiness,
27
35
  ...artifacts,
28
36
  ...intent,
29
37
  ...lifecycle,
@@ -15,6 +15,7 @@ const {
15
15
  const { parseRuntimeState, parseManifest } = require('../schemas');
16
16
  const { syncIntentMemory } = require('../intent');
17
17
  const { normalizeExecutionMode } = require('../lifecycle');
18
+ const { namedError } = require('../errors');
18
19
 
19
20
  async function runApprove({ repoRoot, changeId, executionMode }) {
20
21
  const statePath = getRuntimeStatePath(repoRoot, changeId);
@@ -23,11 +24,25 @@ async function runApprove({ repoRoot, changeId, executionMode }) {
23
24
  const rawManifest = await readJson(manifestPath, null);
24
25
 
25
26
  if (!rawState) {
26
- throw new Error(`Cannot approve ${changeId}: change-state.json is missing`);
27
+ throw namedError(
28
+ 'MissingChangeStateError',
29
+ `Cannot approve ${changeId}: change-state.json is missing`,
30
+ {
31
+ artifactRefs: [statePath],
32
+ rescueAction: 'run cc-roadmap or cc-plan init before approving execution'
33
+ }
34
+ );
27
35
  }
28
36
 
29
37
  if (!rawManifest) {
30
- throw new Error(`Cannot approve ${changeId}: task-manifest.json is missing`);
38
+ throw namedError(
39
+ 'MissingTaskManifestError',
40
+ `Cannot approve ${changeId}: task-manifest.json is missing`,
41
+ {
42
+ artifactRefs: [manifestPath],
43
+ rescueAction: 'run cc-plan to create planning/task-manifest.json before approving execution'
44
+ }
45
+ );
31
46
  }
32
47
 
33
48
  const state = parseRuntimeState(rawState);
@@ -17,6 +17,7 @@ const {
17
17
  } = require('../store');
18
18
  const { parseReportCard, parseManifest } = require('../schemas');
19
19
  const { syncIntentMemory } = require('../intent');
20
+ const { assertShipReady } = require('../readiness');
20
21
 
21
22
  function formatReleaseNote({ changeId, manifest, report }) {
22
23
  const passedTasks = manifest.tasks.filter((task) => task.status === 'passed');
@@ -59,9 +60,11 @@ async function runRelease({ repoRoot, changeId }) {
59
60
  const manifest = parseManifest(await readJson(manifestPath));
60
61
  const previousState = await readJson(statePath, null);
61
62
 
62
- if (report.overall !== 'pass') {
63
- throw new Error('Release blocked: report-card overall is not pass');
64
- }
63
+ assertShipReady(report, {
64
+ reportPath,
65
+ errorName: 'ReleaseReadinessError',
66
+ rescueAction: 'run cc-check until ship-readiness is ready before release'
67
+ });
65
68
 
66
69
  const note = formatReleaseNote({ changeId, manifest, report });
67
70
  const releaseNotePath = getReleaseNotePath(repoRoot, changeId);
@@ -33,6 +33,7 @@ const {
33
33
  const { parseCheckpoint, parseManifest, parseRuntimeState } = require('../schemas');
34
34
  const { applyManifestExecutionState } = require('../planner');
35
35
  const { syncIntentMemory } = require('../intent');
36
+ const { namedError } = require('../errors');
36
37
 
37
38
  function quoteShellArg(value) {
38
39
  return `'${String(value).replace(/'/g, `'\"'\"'`)}'`;
@@ -136,6 +137,34 @@ async function buildTaskBrief(repoRoot, changeId, taskId, planVersion) {
136
137
  ].join('\n');
137
138
  }
138
139
 
140
+ async function assertWorkerPlanFresh(repoRoot, changeId, handoff) {
141
+ const manifestPath = getTaskManifestPath(repoRoot, changeId);
142
+ const manifest = parseManifest(await readJson(manifestPath));
143
+ const currentPlanVersion = manifest.metadata?.planVersion || 1;
144
+
145
+ if (currentPlanVersion === handoff.planVersion) {
146
+ return;
147
+ }
148
+
149
+ throw namedError(
150
+ 'StalePlanVersionError',
151
+ `Worker ${handoff.workerId} was assigned to planVersion ${handoff.planVersion}, but current planVersion is ${currentPlanVersion}`,
152
+ {
153
+ artifactRefs: [
154
+ `devflow/changes/<change>/planning/task-manifest.json`,
155
+ handoff.assignmentPath
156
+ ],
157
+ rescueAction: 'rerun delegation sync for current planVersion before worker-run',
158
+ details: {
159
+ workerId: handoff.workerId,
160
+ assignedPlanVersion: handoff.planVersion,
161
+ currentPlanVersion,
162
+ manifestPath
163
+ }
164
+ }
165
+ );
166
+ }
167
+
139
168
  async function buildProviderPrompt(repoRootOrHandoff, handoffOrTaskId, maybeTaskId) {
140
169
  const repoRoot = typeof repoRootOrHandoff === 'string' ? repoRootOrHandoff : process.cwd();
141
170
  const handoff = typeof repoRootOrHandoff === 'string' ? handoffOrTaskId : repoRootOrHandoff;
@@ -317,6 +346,7 @@ async function runWorkerCommand({
317
346
  }
318
347
 
319
348
  const handoff = await buildWorkerHandoff(repoRoot, changeId, workerId);
349
+ await assertWorkerPlanFresh(repoRoot, changeId, handoff);
320
350
  const primaryTaskId = pickTaskId(handoff, taskId);
321
351
  const assignment = {
322
352
  workerId,
@@ -66,6 +66,14 @@ function parseCsvLike(rawText) {
66
66
  .filter(Boolean);
67
67
  }
68
68
 
69
+ function quoteShellArg(value) {
70
+ return `'${String(value).replace(/'/g, `'\"'\"'`)}'`;
71
+ }
72
+
73
+ function buildTaskRunCommand(taskId, title) {
74
+ return `printf '%s\\n' ${quoteShellArg(`[TASK ${taskId}] ${title}`)}`;
75
+ }
76
+
69
77
  function parseFieldValues(rawValue) {
70
78
  const inlineRefs = parseInlineCodeRefs(rawValue);
71
79
  if (inlineRefs.length > 0) {
@@ -245,7 +253,7 @@ function parseTasksMarkdown(content) {
245
253
  dependsOn,
246
254
  touches,
247
255
  files: inlineFiles,
248
- run: [`echo "[TASK ${taskId}] ${title}"`],
256
+ run: [buildTaskRunCommand(taskId, title)],
249
257
  checks: [],
250
258
  acceptance: [],
251
259
  verification: [],
@@ -420,7 +428,7 @@ function buildDefaultTasks(changeId) {
420
428
  dependsOn: [],
421
429
  touches: [],
422
430
  files: [],
423
- run: [`echo "[TASK T001] Bootstrap ${changeId}"`],
431
+ run: [buildTaskRunCommand('T001', `Bootstrap ${changeId}`)],
424
432
  checks: [],
425
433
  acceptance: ['Bootstrap the requirement workspace'],
426
434
  verification: ['echo "[TASK T001] Bootstrap complete"'],
@@ -0,0 +1,101 @@
1
+ /**
2
+ * [INPUT]: 接收 query id、repoRoot/changeId 与只读 handler registry。
3
+ * [OUTPUT]: 返回 typed query result:ok/data 或 named error,并附 operational trace。
4
+ * [POS]: skill runtime 的查询分发表,只读派生已有 artifact,不承载 workflow 语义。
5
+ * [PROTOCOL]: 变更时更新此头部,然后检查 CLAUDE.md
6
+ */
7
+
8
+ const fs = require('fs');
9
+
10
+ const { namedError, serializeError } = require('./errors');
11
+ const { createTrace } = require('./trace');
12
+
13
+ function resolveArtifactRefs(entry, context, key) {
14
+ const refs = entry[key];
15
+ return typeof refs === 'function' ? refs(context) : refs || [];
16
+ }
17
+
18
+ function createQueryRegistry(entries) {
19
+ const registry = new Map(entries.map((entry) => [entry.id, entry]));
20
+
21
+ function listQueryIds() {
22
+ return [...registry.keys()].sort();
23
+ }
24
+
25
+ async function runQuery(queryId, context = {}) {
26
+ const entry = registry.get(queryId);
27
+
28
+ if (!entry) {
29
+ const supported = listQueryIds();
30
+ const error = namedError(
31
+ 'UnknownQueryError',
32
+ `Unknown query id: ${queryId}`,
33
+ {
34
+ rescueAction: `use one of: ${supported.join(', ')}`,
35
+ details: { supported }
36
+ }
37
+ );
38
+
39
+ return {
40
+ ok: false,
41
+ queryId,
42
+ error: serializeError(error),
43
+ trace: createTrace({
44
+ event: 'query.unknown',
45
+ changeId: context.changeId,
46
+ nextAction: 'choose-supported-query'
47
+ })
48
+ };
49
+ }
50
+
51
+ const artifactRefs = resolveArtifactRefs(entry, context, 'artifactRefs');
52
+ const requiredArtifactRefs = resolveArtifactRefs(entry, context, 'requiredArtifactRefs');
53
+
54
+ try {
55
+ const missingRefs = requiredArtifactRefs.filter((ref) => !fs.existsSync(ref));
56
+ if (missingRefs.length > 0) {
57
+ throw namedError(
58
+ 'MissingQueryArtifactError',
59
+ `Missing required query artifact: ${missingRefs.join(', ')}`,
60
+ {
61
+ artifactRefs: missingRefs,
62
+ rescueAction: 'create required runtime artifacts before running this query'
63
+ }
64
+ );
65
+ }
66
+
67
+ return {
68
+ ok: true,
69
+ queryId,
70
+ data: await entry.handler(context),
71
+ trace: createTrace({
72
+ event: `query.${queryId}`,
73
+ changeId: context.changeId,
74
+ artifactRefs,
75
+ nextAction: entry.nextAction || 'read-query-result'
76
+ })
77
+ };
78
+ } catch (error) {
79
+ return {
80
+ ok: false,
81
+ queryId,
82
+ error: serializeError(error),
83
+ trace: createTrace({
84
+ event: `query.${queryId}.failed`,
85
+ changeId: context.changeId,
86
+ artifactRefs,
87
+ nextAction: error.rescueAction || 'inspect-runtime-artifacts'
88
+ })
89
+ };
90
+ }
91
+ }
92
+
93
+ return {
94
+ listQueryIds,
95
+ runQuery
96
+ };
97
+ }
98
+
99
+ module.exports = {
100
+ createQueryRegistry
101
+ };
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * [INPUT]: 依赖 store/artifacts/lifecycle 读取 requirement 工件,接收 repoRoot 和 changeId。
3
- * [OUTPUT]: 对外提供 getProgress/getNextTask/getFullState 查询函数,作为兼容查询面聚合当前阶段与 PR brief 路径。
3
+ * [OUTPUT]: 对外提供 typed query registry 与兼容查询函数,附 named error 和 trace shape。
4
4
  * [POS]: skill runtime 的薄查询兼容层,只读 artifact 与共享 lifecycle 语义,不再自带流程推导副本。
5
5
  * [PROTOCOL]: 变更时更新此头部,然后检查 CLAUDE.md
6
6
  */
@@ -20,6 +20,44 @@ const {
20
20
  deriveTaskProgress,
21
21
  isTaskCompletedStatus
22
22
  } = require('./lifecycle');
23
+ const { createQueryRegistry } = require('./query-registry');
24
+ const { namedError } = require('./errors');
25
+ const { deriveShipReadiness } = require('./readiness');
26
+
27
+ async function readQueryArtifact(filePath, { required = true } = {}) {
28
+ try {
29
+ const value = await readJson(filePath, null);
30
+
31
+ if (required && value === null) {
32
+ throw namedError(
33
+ 'MissingQueryArtifactError',
34
+ `Missing required query artifact: ${filePath}`,
35
+ {
36
+ artifactRefs: [filePath],
37
+ rescueAction: 'create required runtime artifacts before running this query'
38
+ }
39
+ );
40
+ }
41
+
42
+ return value;
43
+ } catch (error) {
44
+ if (error.name === 'MissingQueryArtifactError') {
45
+ throw error;
46
+ }
47
+
48
+ throw namedError(
49
+ 'InvalidQueryArtifactError',
50
+ `Invalid query artifact ${filePath}: ${error.message}`,
51
+ {
52
+ artifactRefs: [filePath],
53
+ rescueAction: 'repair or regenerate the invalid runtime artifact before running this query',
54
+ details: {
55
+ cause: error.name || 'Error'
56
+ }
57
+ }
58
+ );
59
+ }
60
+ }
23
61
 
24
62
  /**
25
63
  * 获取任务进度统计
@@ -29,21 +67,8 @@ const {
29
67
  */
30
68
  async function getProgress(repoRoot, changeId) {
31
69
  const manifestPath = getTaskManifestPath(repoRoot, changeId);
32
-
33
- try {
34
- const manifest = await readJson(manifestPath);
35
- return deriveTaskProgress(manifest.tasks || []);
36
- } catch (error) {
37
- return {
38
- totalTasks: 0,
39
- completedTasks: 0,
40
- failedTasks: 0,
41
- pendingTasks: 0,
42
- runningTasks: 0,
43
- skippedTasks: 0,
44
- error: error.message
45
- };
46
- }
70
+ const manifest = await readQueryArtifact(manifestPath);
71
+ return deriveTaskProgress(manifest.tasks || []);
47
72
  }
48
73
 
49
74
  /**
@@ -54,41 +79,36 @@ async function getProgress(repoRoot, changeId) {
54
79
  */
55
80
  async function getNextTask(repoRoot, changeId) {
56
81
  const manifestPath = getTaskManifestPath(repoRoot, changeId);
82
+ const manifest = await readQueryArtifact(manifestPath);
83
+ const executionState = deriveManifestExecutionState(manifest.tasks || []);
84
+ const activePhase = manifest.activePhase ?? executionState.activePhase;
85
+ const completedIds = new Set(
86
+ (manifest.tasks || [])
87
+ .filter((task) => isTaskCompletedStatus(task.status))
88
+ .map((task) => task.id)
89
+ );
90
+ const currentTaskId = manifest.currentTaskId ?? executionState.currentTaskId;
57
91
 
58
- try {
59
- const manifest = await readJson(manifestPath);
60
- const executionState = deriveManifestExecutionState(manifest.tasks || []);
61
- const activePhase = manifest.activePhase ?? executionState.activePhase;
62
- const completedIds = new Set(
63
- (manifest.tasks || [])
64
- .filter((task) => isTaskCompletedStatus(task.status))
65
- .map((task) => task.id)
66
- );
67
- const currentTaskId = manifest.currentTaskId ?? executionState.currentTaskId;
68
-
69
- if (currentTaskId) {
70
- const currentTask = manifest.tasks.find((task) => task.id === currentTaskId);
71
- if (currentTask && currentTask.status === 'pending') {
72
- return currentTask;
73
- }
92
+ if (currentTaskId) {
93
+ const currentTask = manifest.tasks.find((task) => task.id === currentTaskId);
94
+ if (currentTask && currentTask.status === 'pending') {
95
+ return currentTask;
74
96
  }
97
+ }
75
98
 
76
- const nextTask = (manifest.tasks || []).find((task) => {
77
- if (task.status !== 'pending') {
78
- return false;
79
- }
99
+ const nextTask = (manifest.tasks || []).find((task) => {
100
+ if (task.status !== 'pending') {
101
+ return false;
102
+ }
80
103
 
81
- if (activePhase !== null && activePhase !== undefined && (task.phase || 1) !== activePhase) {
82
- return false;
83
- }
104
+ if (activePhase !== null && activePhase !== undefined && (task.phase || 1) !== activePhase) {
105
+ return false;
106
+ }
84
107
 
85
- return (task.dependsOn || []).every((depId) => completedIds.has(depId));
86
- });
108
+ return (task.dependsOn || []).every((depId) => completedIds.has(depId));
109
+ });
87
110
 
88
- return nextTask || null;
89
- } catch (error) {
90
- return null;
91
- }
111
+ return nextTask || null;
92
112
  }
93
113
 
94
114
  /**
@@ -102,56 +122,104 @@ async function getFullState(repoRoot, changeId) {
102
122
  const reportPath = getReportCardPath(repoRoot, changeId);
103
123
  const prBriefPath = getIntentPrBriefPath(repoRoot, changeId);
104
124
 
105
- try {
106
- const [state, manifest, hasPrBrief] = await Promise.all([
107
- readJson(statePath),
108
- readJson(getTaskManifestPath(repoRoot, changeId), null),
109
- exists(prBriefPath)
110
- ]);
111
- const progress = await getProgress(repoRoot, changeId);
112
- const nextTask = await getNextTask(repoRoot, changeId);
113
- const report = await readJson(reportPath, null);
114
-
115
- return {
116
- lifecycle: {
117
- changeId: state.changeId,
118
- goal: state.goal,
119
- status: state.status,
120
- initializedAt: state.initializedAt,
121
- plannedAt: state.plannedAt,
122
- verifiedAt: state.verifiedAt,
123
- releasedAt: state.releasedAt,
124
- updatedAt: state.updatedAt,
125
- stage: deriveLifecycleStage({ state, manifest, report, hasPrBrief }),
126
- approval: getApprovalState(state, manifest)
127
- },
128
- progress,
129
- nextTask,
130
- delivery: {
131
- prBriefPath: hasPrBrief ? prBriefPath : null
132
- },
133
- quality: report ? {
134
- overall: report.overall,
135
- verdict: report.verdict || (report.overall === 'pass' ? 'pass' : 'fail'),
136
- reviewStatus: report.review?.status || 'skipped',
137
- reviewFindings: (report.review?.findings || []).length,
138
- blockingFindings: report.blockingFindings,
139
- timestamp: report.timestamp
140
- } : null
141
- };
142
- } catch (error) {
143
- return {
144
- error: error.message,
145
- lifecycle: null,
146
- progress: null,
147
- nextTask: null,
148
- quality: null
149
- };
125
+ const [state, manifest, hasPrBrief] = await Promise.all([
126
+ readQueryArtifact(statePath),
127
+ readQueryArtifact(getTaskManifestPath(repoRoot, changeId)),
128
+ exists(prBriefPath)
129
+ ]);
130
+ const progress = await getProgress(repoRoot, changeId);
131
+ const nextTask = await getNextTask(repoRoot, changeId);
132
+ const report = await readQueryArtifact(reportPath, { required: false });
133
+
134
+ return {
135
+ lifecycle: {
136
+ changeId: state.changeId,
137
+ goal: state.goal,
138
+ status: state.status,
139
+ initializedAt: state.initializedAt,
140
+ plannedAt: state.plannedAt,
141
+ verifiedAt: state.verifiedAt,
142
+ releasedAt: state.releasedAt,
143
+ updatedAt: state.updatedAt,
144
+ stage: deriveLifecycleStage({ state, manifest, report, hasPrBrief }),
145
+ approval: getApprovalState(state, manifest)
146
+ },
147
+ progress,
148
+ nextTask,
149
+ delivery: {
150
+ prBriefPath: hasPrBrief ? prBriefPath : null
151
+ },
152
+ quality: report ? {
153
+ overall: report.overall,
154
+ verdict: report.verdict || (report.overall === 'pass' ? 'pass' : 'fail'),
155
+ reviewStatus: report.review?.status || 'skipped',
156
+ reviewFindings: (report.review?.findings || []).length,
157
+ blockingFindings: report.blockingFindings,
158
+ timestamp: report.timestamp
159
+ } : null
160
+ };
161
+ }
162
+
163
+ async function getShipReadiness(repoRoot, changeId) {
164
+ const reportPath = getReportCardPath(repoRoot, changeId);
165
+ const report = await readJson(reportPath, null);
166
+
167
+ if (!report) {
168
+ throw namedError(
169
+ 'MissingReportCardError',
170
+ `Missing report card for ${changeId}`,
171
+ {
172
+ artifactRefs: [reportPath],
173
+ rescueAction: 'run cc-check and create review/report-card.json before cc-act'
174
+ }
175
+ );
150
176
  }
177
+
178
+ return deriveShipReadiness(report, { reportPath });
151
179
  }
152
180
 
181
+ function queryArtifactRefs(repoRoot, changeId, names) {
182
+ const refs = {
183
+ manifest: getTaskManifestPath(repoRoot, changeId),
184
+ state: getRuntimeStatePath(repoRoot, changeId),
185
+ report: getReportCardPath(repoRoot, changeId),
186
+ prBrief: getIntentPrBriefPath(repoRoot, changeId)
187
+ };
188
+
189
+ return names.map((name) => refs[name]).filter(Boolean);
190
+ }
191
+
192
+ const registry = createQueryRegistry([
193
+ {
194
+ id: 'progress',
195
+ artifactRefs: ({ repoRoot, changeId }) => queryArtifactRefs(repoRoot, changeId, ['manifest']),
196
+ requiredArtifactRefs: ({ repoRoot, changeId }) => queryArtifactRefs(repoRoot, changeId, ['manifest']),
197
+ handler: ({ repoRoot, changeId }) => getProgress(repoRoot, changeId)
198
+ },
199
+ {
200
+ id: 'next-task',
201
+ artifactRefs: ({ repoRoot, changeId }) => queryArtifactRefs(repoRoot, changeId, ['manifest']),
202
+ requiredArtifactRefs: ({ repoRoot, changeId }) => queryArtifactRefs(repoRoot, changeId, ['manifest']),
203
+ handler: ({ repoRoot, changeId }) => getNextTask(repoRoot, changeId)
204
+ },
205
+ {
206
+ id: 'full-state',
207
+ artifactRefs: ({ repoRoot, changeId }) => queryArtifactRefs(repoRoot, changeId, ['state', 'manifest', 'report', 'prBrief']),
208
+ requiredArtifactRefs: ({ repoRoot, changeId }) => queryArtifactRefs(repoRoot, changeId, ['state', 'manifest']),
209
+ handler: ({ repoRoot, changeId }) => getFullState(repoRoot, changeId)
210
+ },
211
+ {
212
+ id: 'ship-readiness',
213
+ artifactRefs: ({ repoRoot, changeId }) => queryArtifactRefs(repoRoot, changeId, ['report']),
214
+ handler: ({ repoRoot, changeId }) => getShipReadiness(repoRoot, changeId)
215
+ }
216
+ ]);
217
+
153
218
  module.exports = {
154
219
  getProgress,
155
220
  getNextTask,
156
- getFullState
221
+ getFullState,
222
+ getShipReadiness,
223
+ listQueryIds: registry.listQueryIds,
224
+ runQuery: registry.runQuery
157
225
  };
@@ -0,0 +1,84 @@
1
+ /**
2
+ * [INPUT]: 接收 report-card 对象与 report artifact path。
3
+ * [OUTPUT]: 派生 ship-readiness 结果,并在未 ready 时抛出 named error。
4
+ * [POS]: skill runtime 的交付就绪单一真相源,被 query/release 共享。
5
+ * [PROTOCOL]: 变更时更新此头部,然后检查 CLAUDE.md
6
+ */
7
+
8
+ const { namedError } = require('./errors');
9
+
10
+ function deriveVerdict(report) {
11
+ return report.verdict || (report.overall === 'pass' ? 'pass' : 'fail');
12
+ }
13
+
14
+ function collectShipReadinessBlockers(report) {
15
+ const verdict = deriveVerdict(report);
16
+ const blockers = [];
17
+
18
+ if (report.overall !== 'pass') {
19
+ blockers.push('report-card overall is not pass');
20
+ }
21
+
22
+ if (verdict !== 'pass') {
23
+ blockers.push(`verdict is ${verdict}`);
24
+ }
25
+
26
+ if ((report.reroute || 'none') !== 'none') {
27
+ blockers.push(`reroute is ${report.reroute}`);
28
+ }
29
+
30
+ if (report.specSyncReady !== true) {
31
+ blockers.push('specSyncReady is not true');
32
+ }
33
+
34
+ blockers.push(...(report.blockingFindings || []));
35
+ blockers.push(...(report.gaps || []));
36
+ return blockers;
37
+ }
38
+
39
+ function deriveShipReadiness(report, { reportPath = '' } = {}) {
40
+ const verdict = deriveVerdict(report);
41
+ const reroute = report.reroute || 'none';
42
+ const specSyncReady = report.specSyncReady === true;
43
+ const blockers = collectShipReadinessBlockers(report);
44
+
45
+ return {
46
+ ready: blockers.length === 0,
47
+ verdict,
48
+ reroute,
49
+ specSyncReady,
50
+ blockers,
51
+ reportPath,
52
+ timestamp: report.timestamp || ''
53
+ };
54
+ }
55
+
56
+ function assertShipReady(report, {
57
+ reportPath = '',
58
+ errorName = 'ShipReadinessError',
59
+ rescueAction = 'run cc-check until ship-readiness is ready'
60
+ } = {}) {
61
+ const readiness = deriveShipReadiness(report, { reportPath });
62
+
63
+ if (readiness.ready) {
64
+ return readiness;
65
+ }
66
+
67
+ throw namedError(
68
+ errorName,
69
+ `Ship readiness blocked: ${readiness.blockers.join('; ')}`,
70
+ {
71
+ artifactRefs: reportPath ? [reportPath] : [],
72
+ rescueAction,
73
+ details: {
74
+ blockers: readiness.blockers
75
+ }
76
+ }
77
+ );
78
+ }
79
+
80
+ module.exports = {
81
+ collectShipReadinessBlockers,
82
+ deriveShipReadiness,
83
+ assertShipReady
84
+ };