cc-devflow 4.5.3 → 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.
- package/.claude/skills/cc-act/CHANGELOG.md +6 -0
- package/.claude/skills/cc-act/PLAYBOOK.md +7 -0
- package/.claude/skills/cc-act/SKILL.md +25 -2
- package/.claude/skills/cc-act/assets/PR_BRIEF_TEMPLATE.md +29 -0
- package/.claude/skills/cc-act/assets/RELEASE_NOTE_TEMPLATE.md +8 -0
- package/.claude/skills/cc-check/CHANGELOG.md +6 -0
- package/.claude/skills/cc-check/PLAYBOOK.md +4 -0
- package/.claude/skills/cc-check/SKILL.md +15 -2
- package/.claude/skills/cc-check/assets/REPORT_CARD_TEMPLATE.json +18 -0
- package/.claude/skills/cc-do/CHANGELOG.md +6 -0
- package/.claude/skills/cc-do/PLAYBOOK.md +6 -4
- package/.claude/skills/cc-do/SKILL.md +14 -5
- package/.claude/skills/cc-do/references/execution-recovery.md +3 -0
- package/.claude/skills/cc-do/references/parallel-dispatch.md +6 -4
- package/.claude/skills/cc-do/scripts/detect-file-conflicts.sh +49 -3
- package/.claude/skills/cc-investigate/CHANGELOG.md +6 -0
- package/.claude/skills/cc-investigate/PLAYBOOK.md +7 -0
- package/.claude/skills/cc-investigate/SKILL.md +10 -1
- package/.claude/skills/cc-investigate/assets/ANALYSIS_TEMPLATE.md +30 -0
- package/.claude/skills/cc-plan/CHANGELOG.md +6 -0
- package/.claude/skills/cc-plan/PLAYBOOK.md +16 -10
- package/.claude/skills/cc-plan/SKILL.md +11 -4
- package/.claude/skills/cc-plan/assets/DESIGN_TEMPLATE.md +41 -0
- package/.claude/skills/cc-plan/assets/TASKS_TEMPLATE.md +4 -0
- package/.claude/skills/cc-plan/assets/TASK_MANIFEST_TEMPLATE.json +32 -3
- package/.claude/skills/cc-plan/assets/TINY_DESIGN_TEMPLATE.md +34 -0
- package/.claude/skills/cc-plan/references/planning-contract.md +11 -3
- package/CHANGELOG.md +8 -0
- package/bin/cc-devflow-cli.js +93 -2
- package/docs/examples/example-bindings.json +5 -5
- package/docs/examples/full-design-blocked/README.md +1 -1
- package/docs/examples/full-design-blocked/changes/REQ-002-bulk-invite-import/planning/design.md +1 -1
- package/docs/examples/full-design-blocked/changes/REQ-002-bulk-invite-import/planning/tasks.md +1 -1
- package/docs/examples/local-handoff/README.md +1 -1
- package/docs/examples/local-handoff/changes/REQ-003-audit-log-export/planning/design.md +1 -1
- package/docs/examples/local-handoff/changes/REQ-003-audit-log-export/planning/tasks.md +1 -1
- package/docs/examples/pdca-loop/README.md +1 -1
- package/docs/examples/pdca-loop/changes/REQ-001-copy-invite-link/planning/design.md +1 -1
- package/docs/examples/pdca-loop/changes/REQ-001-copy-invite-link/planning/task-manifest.json +1 -1
- package/docs/examples/pdca-loop/changes/REQ-001-copy-invite-link/planning/tasks.md +1 -1
- package/docs/get-shit-done-strategy-audit.md +518 -0
- package/lib/compiler/__tests__/inventory.test.js +51 -0
- package/lib/compiler/inventory.js +78 -0
- package/lib/skill-runtime/__tests__/approve.test.js +92 -0
- package/lib/skill-runtime/__tests__/autopilot.test.js +4 -0
- package/lib/skill-runtime/__tests__/planner.tdd.test.js +20 -0
- package/lib/skill-runtime/__tests__/query.test.js +147 -1
- package/lib/skill-runtime/__tests__/readiness.test.js +53 -0
- package/lib/skill-runtime/__tests__/release.test.js +85 -0
- package/lib/skill-runtime/__tests__/runtime.integration.test.js +11 -0
- package/lib/skill-runtime/__tests__/schemas.test.js +56 -0
- package/lib/skill-runtime/__tests__/worker-run.test.js +29 -0
- package/lib/skill-runtime/errors.js +39 -0
- package/lib/skill-runtime/index.js +8 -0
- package/lib/skill-runtime/operations/approve.js +17 -2
- package/lib/skill-runtime/operations/release.js +6 -3
- package/lib/skill-runtime/operations/worker-run.js +30 -0
- package/lib/skill-runtime/planner.js +10 -2
- package/lib/skill-runtime/query-registry.js +101 -0
- package/lib/skill-runtime/query.js +159 -91
- package/lib/skill-runtime/readiness.js +84 -0
- package/lib/skill-runtime/schemas.js +28 -3
- package/lib/skill-runtime/trace.js +22 -0
- package/package.json +1 -1
|
@@ -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: [
|
|
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: [
|
|
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]: 对外提供
|
|
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
|
-
|
|
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
|
-
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
99
|
+
const nextTask = (manifest.tasks || []).find((task) => {
|
|
100
|
+
if (task.status !== 'pending') {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
80
103
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
104
|
+
if (activePhase !== null && activePhase !== undefined && (task.phase || 1) !== activePhase) {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
84
107
|
|
|
85
|
-
|
|
86
|
-
|
|
108
|
+
return (task.dependsOn || []).every((depId) => completedIds.has(depId));
|
|
109
|
+
});
|
|
87
110
|
|
|
88
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
+
};
|
|
@@ -16,6 +16,25 @@ const ChangeIdSchema = z.string().regex(CHANGE_ID_PATTERN, 'Invalid changeId for
|
|
|
16
16
|
const TaskStatusSchema = z.enum(['pending', 'running', 'passed', 'failed', 'skipped']);
|
|
17
17
|
const ReviewDecisionStatusSchema = z.enum(['pending', 'pass', 'fail', 'blocked', 'skipped']);
|
|
18
18
|
|
|
19
|
+
function normalizeTouch(value) {
|
|
20
|
+
return String(value)
|
|
21
|
+
.replace(/\\/g, '/')
|
|
22
|
+
.replace(/\/+/g, '/')
|
|
23
|
+
.replace(/^\.\//, '')
|
|
24
|
+
.replace(/\/$/, '');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function overlappingTouch(left, right) {
|
|
28
|
+
if (left === right) return left;
|
|
29
|
+
if (left && right.startsWith(`${left}/`)) return left;
|
|
30
|
+
if (right && left.startsWith(`${right}/`)) return right;
|
|
31
|
+
return '';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function taskTouches(task) {
|
|
35
|
+
return [...new Set([...(task.touches || []), ...(task.files || [])].map(normalizeTouch).filter(Boolean))];
|
|
36
|
+
}
|
|
37
|
+
|
|
19
38
|
const RuntimeStatusSchema = z.enum(['initialized', 'planned', 'in_progress', 'verified', 'released']);
|
|
20
39
|
const ApprovalStatusSchema = z.enum(['pending', 'approved']);
|
|
21
40
|
const ExecutionModeSchema = z.enum(['direct', 'delegate', 'team']);
|
|
@@ -213,7 +232,7 @@ const ManifestSchema = z.object({
|
|
|
213
232
|
continue;
|
|
214
233
|
}
|
|
215
234
|
|
|
216
|
-
const leftTouches =
|
|
235
|
+
const leftTouches = taskTouches(left);
|
|
217
236
|
|
|
218
237
|
for (let rightIndex = leftIndex + 1; rightIndex < manifest.tasks.length; rightIndex += 1) {
|
|
219
238
|
const right = manifest.tasks[rightIndex];
|
|
@@ -233,8 +252,14 @@ const ManifestSchema = z.object({
|
|
|
233
252
|
});
|
|
234
253
|
}
|
|
235
254
|
|
|
236
|
-
const
|
|
237
|
-
|
|
255
|
+
const rightTouches = taskTouches(right);
|
|
256
|
+
const sharedTouches = [
|
|
257
|
+
...new Set(
|
|
258
|
+
leftTouches.flatMap((leftTouch) =>
|
|
259
|
+
rightTouches.map((rightTouch) => overlappingTouch(leftTouch, rightTouch)).filter(Boolean)
|
|
260
|
+
)
|
|
261
|
+
)
|
|
262
|
+
];
|
|
238
263
|
|
|
239
264
|
if (sharedTouches.length > 0) {
|
|
240
265
|
ctx.addIssue({
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* [INPUT]: 接收 query/doctor/preflight 的事件名、artifact refs 与下一动作。
|
|
3
|
+
* [OUTPUT]: 生成统一 trace shape,供恢复、排查和 report-card 引用。
|
|
4
|
+
* [POS]: skill runtime 的 operational trace 层,不承载 workflow 决策。
|
|
5
|
+
* [PROTOCOL]: 变更时更新此头部,然后检查 CLAUDE.md
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const crypto = require('crypto');
|
|
9
|
+
|
|
10
|
+
function createTrace({ event, changeId, artifactRefs = [], nextAction = 'inspect-result' } = {}) {
|
|
11
|
+
return {
|
|
12
|
+
eventId: `trace-${crypto.randomUUID()}`,
|
|
13
|
+
event: event || 'runtime-event',
|
|
14
|
+
changeId: changeId || '',
|
|
15
|
+
artifactRefs,
|
|
16
|
+
nextAction
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
module.exports = {
|
|
21
|
+
createTrace
|
|
22
|
+
};
|