@chenguangyao/devflow-kit 0.1.43
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/CHANGELOG.md +232 -0
- package/LICENSE +21 -0
- package/README.md +539 -0
- package/bin/devflow.js +9 -0
- package/docs/RFC-001-devflow-kit.md +617 -0
- package/docs/RFC-002-workflow-kernel.md +134 -0
- package/docs/enterprise-integration-supplement.md +274 -0
- package/docs/internal-gitlab-setup.md +426 -0
- package/docs/marketplace-skills.md +231 -0
- package/docs/migration-from-arb.md +232 -0
- package/docs/tooling-overview.md +774 -0
- package/docs/workflow-orchestration.md +695 -0
- package/docs/workflow-ui-prototype.html +271 -0
- package/package.json +52 -0
- package/schemas/config.schema.json +51 -0
- package/schemas/delta.schema.json +22 -0
- package/schemas/state.schema.json +130 -0
- package/schemas/status-surface.schema.json +197 -0
- package/schemas/workflow-confirmation-surface.schema.json +70 -0
- package/schemas/workflow-picker.schema.json +94 -0
- package/scripts/postinstall.js +101 -0
- package/scripts/render-workflow-ui-prototype.js +271 -0
- package/skills/apply/SKILL.md +313 -0
- package/skills/apply/references/discipline-checklist.md +145 -0
- package/skills/apply/references/subagent-implementer-prompt.md +113 -0
- package/skills/apply/references/subagent-orchestration.md +150 -0
- package/skills/apply/references/subagent-reviewer-prompt.md +180 -0
- package/skills/apply/references/tdd-loop.md +287 -0
- package/skills/apply/references/when-plan-is-wrong.md +279 -0
- package/skills/apply/references/worktree-swarm.md +292 -0
- package/skills/archive/SKILL.md +229 -0
- package/skills/archive/references/conflict-resolution.md +336 -0
- package/skills/archive/references/knowledge-deposit.md +381 -0
- package/skills/archive/references/spec-merge.md +365 -0
- package/skills/brainstorm/SKILL.md +123 -0
- package/skills/brainstorm/references/proposal-template.md +244 -0
- package/skills/brainstorm/references/question-catalog.md +168 -0
- package/skills/brainstorm/references/session-template.md +184 -0
- package/skills/ci-fix/SKILL.md +63 -0
- package/skills/ci-fix/references/loop.md +25 -0
- package/skills/code-review/SKILL.md +279 -0
- package/skills/code-review/references/escalation-playbook.md +192 -0
- package/skills/code-review/references/language-cheatsheets/go.md +175 -0
- package/skills/code-review/references/language-cheatsheets/java-spring-mybatis.md +246 -0
- package/skills/code-review/references/language-cheatsheets/python.md +170 -0
- package/skills/code-review/references/language-cheatsheets/vue.md +199 -0
- package/skills/code-review/references/output-template.md +275 -0
- package/skills/code-review/references/review-checklist.md +251 -0
- package/skills/complexity-grading/SKILL.md +259 -0
- package/skills/deliver/SKILL.md +271 -0
- package/skills/deliver/references/delivery-modes.md +299 -0
- package/skills/deliver/references/notify.md +359 -0
- package/skills/deliver/references/pr-description.md +319 -0
- package/skills/dependency-upgrade/SKILL.md +57 -0
- package/skills/dependency-upgrade/references/risk-matrix.md +38 -0
- package/skills/df-orchestrator/SKILL.md +407 -0
- package/skills/df-orchestrator/references/complexity-grading.md +177 -0
- package/skills/df-orchestrator/references/escalation-matrix.md +191 -0
- package/skills/df-orchestrator/references/routing-rules.md +290 -0
- package/skills/df-orchestrator/references/workflow-state-machine.md +208 -0
- package/skills/frontend-quality/SKILL.md +61 -0
- package/skills/frontend-quality/references/checklist.md +35 -0
- package/skills/handoff-resume/SKILL.md +59 -0
- package/skills/handoff-resume/references/handoff-template.md +54 -0
- package/skills/plan/SKILL.md +166 -0
- package/skills/plan/references/task-breakdown.md +207 -0
- package/skills/plan/references/task-sequencing.md +143 -0
- package/skills/plan/references/task-template.md +248 -0
- package/skills/requirement-analysis/SKILL.md +499 -0
- package/skills/requirement-analysis/references/acceptance-criteria.md +183 -0
- package/skills/requirement-analysis/references/code-recon.md +151 -0
- package/skills/requirement-analysis/references/edge-case-catalog.md +164 -0
- package/skills/requirement-analysis/references/requirement-template.md +339 -0
- package/skills/requirement-analysis/references/scope-negotiation.md +162 -0
- package/skills/security-hardening/SKILL.md +60 -0
- package/skills/security-hardening/references/checklist.md +42 -0
- package/skills/tech-spec/SKILL.md +388 -0
- package/skills/tech-spec/references/api-contract-design.md +172 -0
- package/skills/tech-spec/references/decision-records.md +110 -0
- package/skills/tech-spec/references/design-template.md +301 -0
- package/skills/tech-spec/references/rollout-and-rollback.md +203 -0
- package/skills/tech-spec/references/spec-delta-conventions.md +250 -0
- package/skills/tech-spec/references/transaction-patterns.md +212 -0
- package/skills/test-spec/SKILL.md +219 -0
- package/skills/test-spec/references/coverage-strategy.md +218 -0
- package/skills/test-spec/references/edge-case-to-test.md +143 -0
- package/skills/test-spec/references/test-case-template.md +276 -0
- package/skills/verify/SKILL.md +232 -0
- package/skills/verify/references/nfr-verification.md +292 -0
- package/skills/verify/references/report-templates.md +510 -0
- package/skills/verify/references/self-test-guide.md +240 -0
- package/skills/verify/references/verify-rollback-map.md +247 -0
- package/src/cli/commands/_helpers.js +108 -0
- package/src/cli/commands/_submit.js +718 -0
- package/src/cli/commands/apply.js +198 -0
- package/src/cli/commands/archive.js +180 -0
- package/src/cli/commands/checkpoint.js +113 -0
- package/src/cli/commands/deliver.js +377 -0
- package/src/cli/commands/deploy.js +504 -0
- package/src/cli/commands/design.js +158 -0
- package/src/cli/commands/disable.js +21 -0
- package/src/cli/commands/doctor.js +178 -0
- package/src/cli/commands/enable.js +21 -0
- package/src/cli/commands/flow.js +645 -0
- package/src/cli/commands/help.js +93 -0
- package/src/cli/commands/ingest.js +602 -0
- package/src/cli/commands/init.js +341 -0
- package/src/cli/commands/knowledge.js +523 -0
- package/src/cli/commands/logs.js +43 -0
- package/src/cli/commands/new.js +202 -0
- package/src/cli/commands/plan.js +49 -0
- package/src/cli/commands/propose.js +27 -0
- package/src/cli/commands/provider.js +698 -0
- package/src/cli/commands/report.js +143 -0
- package/src/cli/commands/requirement.js +227 -0
- package/src/cli/commands/review.js +301 -0
- package/src/cli/commands/skills.js +457 -0
- package/src/cli/commands/status.js +925 -0
- package/src/cli/commands/switch.js +27 -0
- package/src/cli/commands/sync.js +47 -0
- package/src/cli/commands/test.js +366 -0
- package/src/cli/commands/uninstall.js +32 -0
- package/src/cli/commands/update.js +74 -0
- package/src/cli/commands/verify.js +354 -0
- package/src/cli/commands/worktree.js +78 -0
- package/src/cli/index.js +72 -0
- package/src/cli/parse-args.js +102 -0
- package/src/core/autodetect.js +271 -0
- package/src/core/change.js +208 -0
- package/src/core/checkpoint.js +217 -0
- package/src/core/config.js +60 -0
- package/src/core/delta.js +290 -0
- package/src/core/markers.js +59 -0
- package/src/core/paths.js +173 -0
- package/src/core/plan-tasks.js +36 -0
- package/src/core/project-routing.js +285 -0
- package/src/core/projects.js +200 -0
- package/src/core/state.js +200 -0
- package/src/core/workflow-check.js +177 -0
- package/src/core/workflow-init.js +34 -0
- package/src/core/workflow-picker.js +154 -0
- package/src/core/workflow-policy.js +119 -0
- package/src/core/workflow-suggest.js +181 -0
- package/src/core/workflow-verify.js +88 -0
- package/src/core/workflow.js +433 -0
- package/src/core/worktree.js +241 -0
- package/src/knowledge/categories.js +107 -0
- package/src/knowledge/classify.js +125 -0
- package/src/knowledge/deposit.js +414 -0
- package/src/knowledge/migrate.js +149 -0
- package/src/knowledge/mr.js +219 -0
- package/src/knowledge/query.js +131 -0
- package/src/knowledge/registry.js +151 -0
- package/src/knowledge/sync.js +179 -0
- package/src/providers/base.js +74 -0
- package/src/providers/drivers/api-yapi.js +78 -0
- package/src/providers/drivers/ci-jenkins.js +109 -0
- package/src/providers/drivers/intake-confluence.js +544 -0
- package/src/providers/drivers/kb-git.js +549 -0
- package/src/providers/drivers/kb-weknora.js +472 -0
- package/src/providers/drivers/notify-smtp.js +515 -0
- package/src/providers/drivers/observability-oss.js +43 -0
- package/src/providers/drivers/observability-sls.js +50 -0
- package/src/providers/lifecycle.js +135 -0
- package/src/providers/loader.js +132 -0
- package/src/providers/local.js +190 -0
- package/src/providers/userconfig.js +283 -0
- package/src/reports/aggregate.js +185 -0
- package/src/reports/coverage.js +163 -0
- package/src/reports/detect.js +143 -0
- package/src/reports/parse.js +236 -0
- package/src/templates/files/ci/github.yml +38 -0
- package/src/templates/files/ci/gitlab.yml +27 -0
- package/src/templates/files/design.md +63 -0
- package/src/templates/files/ide/devflow-workflow.md +58 -0
- package/src/templates/files/ide/project-overview-reference.md +1 -0
- package/src/templates/files/ide/project-overview.md +27 -0
- package/src/templates/files/knowledge-index.json +17 -0
- package/src/templates/files/knowledge.md +28 -0
- package/src/templates/files/meta.json +8 -0
- package/src/templates/files/plan.md +38 -0
- package/src/templates/files/proposal.md +33 -0
- package/src/templates/files/reports/contract-test.md +40 -0
- package/src/templates/files/reports/e2e-test.md +30 -0
- package/src/templates/files/reports/integration-test.md +36 -0
- package/src/templates/files/reports/joint-test.md +58 -0
- package/src/templates/files/reports/perf.md +24 -0
- package/src/templates/files/reports/regression.md +20 -0
- package/src/templates/files/reports/remote-test.md +55 -0
- package/src/templates/files/reports/self-test.md +43 -0
- package/src/templates/files/reports/smoke-test.md +22 -0
- package/src/templates/files/reports/unit-test.md +36 -0
- package/src/templates/files/requirement.md +51 -0
- package/src/templates/files/review.md +38 -0
- package/src/templates/files/tests.md +36 -0
- package/src/templates/files/verify.md +32 -0
- package/src/templates/index.js +21 -0
- package/src/utils/log.js +37 -0
|
@@ -0,0 +1,925 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const fs = require('fs/promises');
|
|
3
|
+
const fsSync = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const log = require('../../utils/log.js');
|
|
6
|
+
const paths = require('../../core/paths.js');
|
|
7
|
+
const change = require('../../core/change.js');
|
|
8
|
+
const state = require('../../core/state.js');
|
|
9
|
+
const checkpoint = require('../../core/checkpoint.js');
|
|
10
|
+
const workflowCheck = require('../../core/workflow-check.js');
|
|
11
|
+
const workflowVerify = require('../../core/workflow-verify.js');
|
|
12
|
+
const aggregate = require('../../reports/aggregate.js');
|
|
13
|
+
const helpers = require('./_helpers.js');
|
|
14
|
+
|
|
15
|
+
const CORE_ARTIFACTS = [
|
|
16
|
+
'proposal.md',
|
|
17
|
+
'requirement.md',
|
|
18
|
+
'design.md',
|
|
19
|
+
'plan.md',
|
|
20
|
+
'review.md',
|
|
21
|
+
'verify.md',
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
const OPTIONAL_ARTIFACTS = [
|
|
25
|
+
'tests.md',
|
|
26
|
+
'knowledge.md',
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
async function run({ positional = [], flags = {}, cwd }) {
|
|
30
|
+
const root = cwd || process.cwd();
|
|
31
|
+
if (!fsSync.existsSync(paths.devflowDir(root))) {
|
|
32
|
+
log.warn('not a devflow project. run "devflow init" first.');
|
|
33
|
+
process.exitCode = 1;
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (positional[0] === 'set-level') {
|
|
38
|
+
await runSetLevel(root, positional.slice(1), flags);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (positional[0] === 'risk') {
|
|
42
|
+
await runRisk(root, positional.slice(1), flags);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const slug = positional[0] || flags.slug || (await change.getCurrent(root));
|
|
47
|
+
if (!slug) {
|
|
48
|
+
log.warn('no change found. run "devflow ingest <input>" or "devflow new <slug>".');
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let st;
|
|
53
|
+
try { st = await state.read(root, slug); } catch (e) {
|
|
54
|
+
log.error(`cannot read state for ${slug}: ${e.message}`);
|
|
55
|
+
process.exitCode = 1;
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (flags.json === true) {
|
|
60
|
+
log.raw(JSON.stringify(buildStatusSurface(root, slug, st), null, 2));
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const { colors } = log;
|
|
65
|
+
log.raw('');
|
|
66
|
+
log.raw(`change ${colors.bold(slug)} level=${colors.cyan(st.level)} phase=${colors.magenta(st.currentPhase)}`);
|
|
67
|
+
if (st.title && st.title !== slug) log.dim(` title: ${st.title}`);
|
|
68
|
+
if (st.source && st.source.ref) log.dim(` source: ${st.source.type}:${st.source.ref}`);
|
|
69
|
+
let workflowCheckResult = null;
|
|
70
|
+
if (st.workflow && st.workflow.baseRecipe) {
|
|
71
|
+
const wf = st.workflow;
|
|
72
|
+
const next = nextWorkflowStep(wf);
|
|
73
|
+
log.dim(` workflow: ${wf.baseRecipe.label || wf.baseRecipe.id} (${wf.baseRecipe.id}) status=${wf.status || 'draft'} current=${wf.currentStep || '-'} next=${next || '-'}`);
|
|
74
|
+
const overrides = summarizeWorkflowOverrides(wf);
|
|
75
|
+
if (overrides.length) log.dim(` overrides: ${overrides.join('; ')}`);
|
|
76
|
+
if ((wf.status || 'draft') !== 'confirmed') {
|
|
77
|
+
workflowCheckResult = workflowCheck.checkWorkflow(root, slug, st);
|
|
78
|
+
renderWorkflowCheck(slug, workflowCheckResult);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
log.raw('');
|
|
82
|
+
|
|
83
|
+
log.raw(colors.bold('phases'));
|
|
84
|
+
for (const p of state.PHASES) {
|
|
85
|
+
const ps = (st.phases && st.phases[p]) || { status: 'pending' };
|
|
86
|
+
const sym = ps.status === 'completed' ? colors.green('✓')
|
|
87
|
+
: ps.status === 'in_progress' ? colors.yellow('●')
|
|
88
|
+
: ps.status === 'skipped' ? colors.dim('-')
|
|
89
|
+
: colors.dim('·');
|
|
90
|
+
log.raw(` ${sym} ${p.padEnd(12)} ${colors.dim(ps.ts || '')}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
log.raw('');
|
|
94
|
+
log.raw(colors.bold('core artifacts'));
|
|
95
|
+
const cdir = change.resolveChangeDir(root, slug);
|
|
96
|
+
for (const name of CORE_ARTIFACTS) {
|
|
97
|
+
const exists = fsSync.existsSync(path.join(cdir, name));
|
|
98
|
+
log.raw(` ${exists ? colors.green('✓') : colors.dim('·')} ${name}`);
|
|
99
|
+
}
|
|
100
|
+
log.raw(colors.bold('optional artifacts'));
|
|
101
|
+
for (const name of OPTIONAL_ARTIFACTS) {
|
|
102
|
+
const exists = fsSync.existsSync(path.join(cdir, name));
|
|
103
|
+
log.raw(` ${exists ? colors.green('✓') : colors.dim('·')} ${name}`);
|
|
104
|
+
}
|
|
105
|
+
// delta dir
|
|
106
|
+
const deltaDir = path.join(cdir, 'delta');
|
|
107
|
+
const deltaCnt = fsSync.existsSync(deltaDir) ? fsSync.readdirSync(deltaDir).filter((f) => f.endsWith('.md')).length : 0;
|
|
108
|
+
log.raw(` ${deltaCnt > 0 ? colors.green('✓') : colors.dim('·')} delta/ (${deltaCnt} file${deltaCnt === 1 ? '' : 's'})`);
|
|
109
|
+
// reports dir
|
|
110
|
+
const reportsDir = path.join(cdir, 'reports');
|
|
111
|
+
const reportCnt = fsSync.existsSync(reportsDir) ? fsSync.readdirSync(reportsDir).filter((f) => f.endsWith('.md')).length : 0;
|
|
112
|
+
log.raw(` ${reportCnt > 0 ? colors.green('✓') : colors.dim('·')} reports/ (${reportCnt} file${reportCnt === 1 ? '' : 's'})`);
|
|
113
|
+
|
|
114
|
+
// Apply tasks (worktree swarm)
|
|
115
|
+
const applyTasks = (st.phases && st.phases.apply && st.phases.apply.tasks) || [];
|
|
116
|
+
if (applyTasks.length) {
|
|
117
|
+
log.raw('');
|
|
118
|
+
log.raw(colors.bold(`apply tasks`) + colors.dim(` (iterations=${(st.iterations && st.iterations.apply) || 0})`));
|
|
119
|
+
for (const t of applyTasks) {
|
|
120
|
+
const sym = t.status === 'done' ? colors.green('✓')
|
|
121
|
+
: t.status === 'in_progress' ? colors.yellow('●')
|
|
122
|
+
: t.status === 'failed' ? colors.red('×')
|
|
123
|
+
: t.status === 'skipped' ? colors.dim('-')
|
|
124
|
+
: colors.dim('·');
|
|
125
|
+
const wt = t.worktree ? path.relative(root, t.worktree) : '(no worktree)';
|
|
126
|
+
log.raw(` ${sym} ${String(t.id).padEnd(12)} ${colors.dim(t.status.padEnd(12))} ${colors.cyan(t.branch || '')}`);
|
|
127
|
+
log.dim(` ${wt} · ${t.title || ''} · notes=${(t.notes || []).length}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Review rounds
|
|
132
|
+
const review = (st.phases && st.phases.review) || {};
|
|
133
|
+
const rounds = review.rounds || [];
|
|
134
|
+
if (rounds.length || review.status !== 'pending') {
|
|
135
|
+
log.raw('');
|
|
136
|
+
const header = `review rounds` + colors.dim(` (${rounds.length}/${review.max_rounds || 3}, status=${review.status || 'pending'})`);
|
|
137
|
+
log.raw(colors.bold(header));
|
|
138
|
+
for (const r of rounds) {
|
|
139
|
+
const sym = r.outcome === 'pass' ? colors.green('✓')
|
|
140
|
+
: r.outcome === 'blocked' ? colors.red('×')
|
|
141
|
+
: r.outcome === 'back_to_apply' ? colors.yellow('↺')
|
|
142
|
+
: colors.dim('·');
|
|
143
|
+
const tail = r.source === 'force-pass' ? colors.magenta(` force-pass: ${r.reason || ''}`) : '';
|
|
144
|
+
log.raw(` ${sym} round ${r.round} must=${r.must || 0} should=${r.should || 0} nit=${r.nit || 0} ${colors.dim(r.outcome)}${tail}`);
|
|
145
|
+
}
|
|
146
|
+
if (review.status === 'blocked') {
|
|
147
|
+
log.raw('');
|
|
148
|
+
log.raw(colors.red(' review BLOCKED. options:'));
|
|
149
|
+
log.dim(` - fix remaining MUSTs + devflow review --slug=${slug} --round --continue-rounds`);
|
|
150
|
+
log.dim(` - devflow review --slug=${slug} --force-pass --reason="..." (audited)`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (st.checkpoints && st.checkpoints.length) {
|
|
155
|
+
log.raw('');
|
|
156
|
+
log.raw(colors.bold('checkpoints'));
|
|
157
|
+
for (const c of st.checkpoints) {
|
|
158
|
+
const status = c.status || 'pending';
|
|
159
|
+
const sym = status === 'passed' || status === 'resolved' ? colors.green('✓')
|
|
160
|
+
: status === 'pending' ? colors.yellow('●')
|
|
161
|
+
: colors.red('×');
|
|
162
|
+
log.raw(` ${sym} ${c.name || c.id || c.type} ${colors.dim(status)}`);
|
|
163
|
+
if (c.question) log.dim(` ${c.question}`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const risks = st.riskSignals || [];
|
|
168
|
+
if (risks.length) {
|
|
169
|
+
log.raw('');
|
|
170
|
+
log.raw(colors.bold('risk signals'));
|
|
171
|
+
for (const r of risks) {
|
|
172
|
+
const status = r.status || 'open';
|
|
173
|
+
const sym = status === 'resolved' ? colors.green('✓')
|
|
174
|
+
: status === 'accepted' ? colors.magenta('!')
|
|
175
|
+
: colors.yellow('●');
|
|
176
|
+
log.raw(` ${sym} ${String(r.type).padEnd(22)} ${colors.dim(status)} ${r.reason || ''}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
log.raw('');
|
|
181
|
+
hintNext(st, colors, slug, workflowCheckResult);
|
|
182
|
+
const pending = checkpoint.latestPending(st);
|
|
183
|
+
if (pending) {
|
|
184
|
+
log.raw('');
|
|
185
|
+
log.raw(checkpoint.renderNextStepCard(pending));
|
|
186
|
+
}
|
|
187
|
+
log.raw('');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function runRisk(root, positional = [], flags = {}) {
|
|
191
|
+
const action = positional[0] || 'list';
|
|
192
|
+
const type = positional[1] || flags.type;
|
|
193
|
+
const slug = flags.slug || positional[2] || (await change.getCurrent(root));
|
|
194
|
+
if (!slug) {
|
|
195
|
+
log.warn('no change found. pass --slug=<slug>.');
|
|
196
|
+
process.exitCode = 1;
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
let st;
|
|
201
|
+
try { st = await state.read(root, slug); } catch (e) {
|
|
202
|
+
log.error(`cannot read state for ${slug}: ${e.message}`);
|
|
203
|
+
process.exitCode = 1;
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (action === 'list') {
|
|
208
|
+
const risks = st.riskSignals || [];
|
|
209
|
+
if (!risks.length) {
|
|
210
|
+
log.dim('no risk signals.');
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
for (const r of risks) log.raw(` - ${r.type} status=${r.status || 'open'} reason=${r.reason || '-'}`);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (!type) {
|
|
218
|
+
log.error('usage: devflow status risk <add|resolve|accept|list> <type> --slug=<slug> [--reason="..."]');
|
|
219
|
+
process.exitCode = 2;
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const reason = flags.reason && flags.reason !== true ? flags.reason : '';
|
|
224
|
+
if (action === 'add') {
|
|
225
|
+
if (!reason) {
|
|
226
|
+
log.error('status risk add requires --reason="..."');
|
|
227
|
+
process.exitCode = 2;
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
state.addRiskSignal(st, { type, reason, source: flags.source || 'cli' });
|
|
231
|
+
state.logEvent(st, 'risk_signal.add', { type, reason, source: flags.source || 'cli' });
|
|
232
|
+
await state.write(root, slug, st);
|
|
233
|
+
log.ok(`risk signal added: ${type}`);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (action === 'resolve') {
|
|
238
|
+
state.resolveRiskSignal(st, type, { reason: reason || 'resolved via CLI', evidence: parseEvidence(flags.evidence) });
|
|
239
|
+
state.logEvent(st, 'risk_signal.resolve', { type, reason: reason || 'resolved via CLI' });
|
|
240
|
+
await state.write(root, slug, st);
|
|
241
|
+
log.ok(`risk signal resolved: ${type}`);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (action === 'accept') {
|
|
246
|
+
if (!reason) {
|
|
247
|
+
log.error('status risk accept requires --reason="..."');
|
|
248
|
+
process.exitCode = 2;
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
state.acceptRiskSignal(st, type, { reason, evidence: parseEvidence(flags.evidence) });
|
|
252
|
+
state.logEvent(st, 'risk_signal.accept', { type, reason });
|
|
253
|
+
await state.write(root, slug, st);
|
|
254
|
+
log.warn(`risk signal accepted: ${type}`);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
log.error('usage: devflow status risk <add|resolve|accept|list> <type> --slug=<slug> [--reason="..."]');
|
|
259
|
+
process.exitCode = 2;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function parseEvidence(v) {
|
|
263
|
+
if (!v || v === true) return [];
|
|
264
|
+
if (Array.isArray(v)) return v.flatMap(parseEvidence);
|
|
265
|
+
return String(v).split(',').map((s) => s.trim()).filter(Boolean);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async function runSetLevel(root, positional = [], flags = {}) {
|
|
269
|
+
const nextLevel = positional[0] || flags.level;
|
|
270
|
+
if (!['L0', 'L1', 'L2', 'L3'].includes(nextLevel)) {
|
|
271
|
+
log.error('usage: devflow status set-level <L0|L1|L2|L3> --slug=<slug> --reason="..."');
|
|
272
|
+
process.exitCode = 2;
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
const reason = flags.reason;
|
|
276
|
+
if (!reason || reason === true) {
|
|
277
|
+
log.error('status set-level requires --reason="..."');
|
|
278
|
+
process.exitCode = 2;
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const slug = flags.slug || positional[1] || (await change.getCurrent(root));
|
|
283
|
+
if (!slug) {
|
|
284
|
+
log.warn('no change found. run "devflow ingest <input>" or "devflow new <slug>".');
|
|
285
|
+
process.exitCode = 1;
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
let st;
|
|
290
|
+
try { st = await state.read(root, slug); } catch (e) {
|
|
291
|
+
log.error(`cannot read state for ${slug}: ${e.message}`);
|
|
292
|
+
process.exitCode = 1;
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const previousLevel = st.level || 'L2';
|
|
297
|
+
const wasMicro = st.mode === 'micro' || previousLevel === 'L0';
|
|
298
|
+
st.level = nextLevel;
|
|
299
|
+
st.enabled = state.applyLevelDefaults(nextLevel, { ...state.DEFAULT_ENABLED });
|
|
300
|
+
|
|
301
|
+
if (nextLevel === 'L0') {
|
|
302
|
+
st.mode = 'micro';
|
|
303
|
+
setRawPhase(st, 'requirement', 'skipped', 'micro mode');
|
|
304
|
+
setRawPhase(st, 'design', 'skipped', 'micro mode');
|
|
305
|
+
setRawPhase(st, 'archive', 'skipped', 'micro mode');
|
|
306
|
+
state.setPhase(st, 'plan', 'in_progress');
|
|
307
|
+
} else if (wasMicro) {
|
|
308
|
+
delete st.mode;
|
|
309
|
+
setRawPhase(st, 'requirement', 'pending');
|
|
310
|
+
setRawPhase(st, 'design', 'pending');
|
|
311
|
+
setRawPhase(st, 'archive', 'pending');
|
|
312
|
+
st.currentPhase = 'requirement';
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
state.logEvent(st, 'level changed', { from: previousLevel, to: nextLevel, reason });
|
|
316
|
+
await state.write(root, slug, st);
|
|
317
|
+
log.ok(`level: ${previousLevel} -> ${nextLevel}`);
|
|
318
|
+
if (wasMicro && nextLevel !== 'L0') {
|
|
319
|
+
log.dim('next: run devflow requirement to enter the standard route.');
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function setRawPhase(st, phase, status, reason) {
|
|
324
|
+
st.phases = st.phases || {};
|
|
325
|
+
st.phases[phase] = { status, ts: new Date().toISOString() };
|
|
326
|
+
if (reason) st.phases[phase].reason = reason;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function renderWorkflowCheck(slug, result) {
|
|
330
|
+
const errors = result.errors || [];
|
|
331
|
+
const warnings = result.warnings || [];
|
|
332
|
+
const issueCount = errors.length + warnings.length;
|
|
333
|
+
log.dim(` workflow card: devflow flow card --slug=${slug} --json`);
|
|
334
|
+
log.dim(` workflow card: ok=${result.ok} status=${result.status || '-'} errors=${errors.length} warnings=${warnings.length}`);
|
|
335
|
+
for (const item of [...errors, ...warnings].slice(0, 3)) {
|
|
336
|
+
log.dim(` - ${item.code}: ${item.summary}`);
|
|
337
|
+
}
|
|
338
|
+
if (issueCount > 3) log.dim(` - ... ${issueCount - 3} more`);
|
|
339
|
+
if (result.nextAction) log.dim(` nextAction: ${result.nextAction}`);
|
|
340
|
+
if (result.ok && result.confirmationCard) helpers.renderWorkflowConfirmationCard(result.confirmationCard);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function buildStatusSurface(root, slug, st) {
|
|
344
|
+
const wf = st.workflow || null;
|
|
345
|
+
const workflowCheckResult = wf && wf.baseRecipe
|
|
346
|
+
? workflowCheck.checkWorkflow(root, slug, st)
|
|
347
|
+
: null;
|
|
348
|
+
const pending = checkpoint.latestPending(st);
|
|
349
|
+
const cdir = change.resolveChangeDir(root, slug);
|
|
350
|
+
const checkpointConfirmationCard = pending ? checkpoint.buildConfirmationCard(pending, checkpointCardOptions(pending)) : null;
|
|
351
|
+
const guidance = buildStatusGuidance(slug, st, workflowCheckResult, pending, checkpointConfirmationCard);
|
|
352
|
+
return {
|
|
353
|
+
type: 'change_status_surface',
|
|
354
|
+
slug,
|
|
355
|
+
title: st.title || slug,
|
|
356
|
+
level: st.level || null,
|
|
357
|
+
phase: st.currentPhase || null,
|
|
358
|
+
source: st.source || null,
|
|
359
|
+
phases: buildPhaseSurface(st),
|
|
360
|
+
artifacts: buildArtifactSurface(cdir),
|
|
361
|
+
apply: buildApplySurface(st),
|
|
362
|
+
review: buildReviewSurface(st),
|
|
363
|
+
verify: buildVerifySurface(st, cdir),
|
|
364
|
+
riskSignals: buildRiskSignalSurface(st),
|
|
365
|
+
workflow: wf ? {
|
|
366
|
+
status: wf.status || null,
|
|
367
|
+
baseRecipe: wf.baseRecipe || null,
|
|
368
|
+
currentStep: wf.currentStep || null,
|
|
369
|
+
nextStep: nextWorkflowStep(wf),
|
|
370
|
+
overrides: summarizeWorkflowOverrides(wf),
|
|
371
|
+
check: workflowCheckResult,
|
|
372
|
+
actions: {
|
|
373
|
+
card: `devflow flow card --slug=${slug} --json`,
|
|
374
|
+
picker: `devflow flow picker --slug=${slug} --json`,
|
|
375
|
+
confirm: `devflow flow confirm --slug=${slug}`,
|
|
376
|
+
},
|
|
377
|
+
} : null,
|
|
378
|
+
pendingCheckpoint: pending ? checkpoint.summarizeCheckpoint(pending) : null,
|
|
379
|
+
checkpointConfirmationCard,
|
|
380
|
+
primaryPanel: guidance.primaryPanel,
|
|
381
|
+
blockingReason: guidance.blockingReason,
|
|
382
|
+
availableActions: guidance.availableActions,
|
|
383
|
+
nextAction: guidance.nextAction,
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function buildStatusGuidance(slug, st, workflowCheckResult, pending, checkpointConfirmationCard) {
|
|
388
|
+
if (pending) {
|
|
389
|
+
const actions = [
|
|
390
|
+
surfaceAction('show-checkpoint', '查看确认卡', `devflow checkpoint show --slug=${slug} --json`, {
|
|
391
|
+
primary: true,
|
|
392
|
+
kind: 'open-surface',
|
|
393
|
+
}),
|
|
394
|
+
...actionsFromConfirmationCard(checkpointConfirmationCard, 'checkpoint-decision'),
|
|
395
|
+
];
|
|
396
|
+
return {
|
|
397
|
+
primaryPanel: {
|
|
398
|
+
type: 'checkpoint',
|
|
399
|
+
title: checkpointConfirmationCard ? checkpointConfirmationCard.title : '确认下一步',
|
|
400
|
+
status: 'needs_input',
|
|
401
|
+
reasonCode: 'pending-checkpoint',
|
|
402
|
+
checkpointType: pending.type,
|
|
403
|
+
checkpointId: pending.id,
|
|
404
|
+
actionIds: actions.map((action) => action.id),
|
|
405
|
+
},
|
|
406
|
+
blockingReason: {
|
|
407
|
+
code: 'pending-checkpoint',
|
|
408
|
+
source: 'checkpoint',
|
|
409
|
+
severity: 'blocker',
|
|
410
|
+
message: pending.summary || `pending ${pending.type} checkpoint`,
|
|
411
|
+
},
|
|
412
|
+
availableActions: actions,
|
|
413
|
+
nextAction: `devflow checkpoint show --slug=${slug} --json`,
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const workflowIssues = workflowCheckIssues(workflowCheckResult);
|
|
418
|
+
const wf = st.workflow || null;
|
|
419
|
+
if (workflowCheckResult && workflowIssues.length) {
|
|
420
|
+
const issue = workflowIssues[0];
|
|
421
|
+
const action = actionFromCommand('fix-workflow', workflowIssueActionLabel(issue), workflowCheckResult.nextAction, { primary: true, kind: 'workflow' });
|
|
422
|
+
const actions = compactActions([
|
|
423
|
+
action,
|
|
424
|
+
surfaceAction('edit-workflow', '调整编排', `devflow flow picker --slug=${slug} --json`, { kind: 'open-surface' }),
|
|
425
|
+
surfaceAction('view-workflow-card', '查看 workflow 卡片', `devflow flow card --slug=${slug} --json`, { kind: 'open-surface' }),
|
|
426
|
+
]);
|
|
427
|
+
return {
|
|
428
|
+
primaryPanel: {
|
|
429
|
+
type: 'workflow_check',
|
|
430
|
+
title: '调整 workflow',
|
|
431
|
+
status: 'needs_input',
|
|
432
|
+
reasonCode: issue.code || 'workflow-check-failed',
|
|
433
|
+
actionIds: actions.map((item) => item.id),
|
|
434
|
+
},
|
|
435
|
+
blockingReason: {
|
|
436
|
+
code: issue.code || 'workflow-check-failed',
|
|
437
|
+
source: 'workflow',
|
|
438
|
+
severity: workflowCheckResult.errors && workflowCheckResult.errors.length ? 'blocker' : 'warning',
|
|
439
|
+
message: issue.summary || issue.message || 'workflow check failed',
|
|
440
|
+
},
|
|
441
|
+
availableActions: actions,
|
|
442
|
+
nextAction: workflowCheckResult.nextAction || null,
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
if (wf && (wf.status || 'draft') !== 'confirmed') {
|
|
447
|
+
const actions = [
|
|
448
|
+
surfaceAction('confirm-workflow', '确认 workflow', `devflow flow confirm --slug=${slug}`, { primary: true, kind: 'workflow' }),
|
|
449
|
+
surfaceAction('edit-workflow', '调整编排', `devflow flow picker --slug=${slug} --json`, { kind: 'open-surface' }),
|
|
450
|
+
surfaceAction('view-workflow-card', '查看 workflow 卡片', `devflow flow card --slug=${slug} --json`, { kind: 'open-surface' }),
|
|
451
|
+
];
|
|
452
|
+
return {
|
|
453
|
+
primaryPanel: {
|
|
454
|
+
type: 'workflow_confirm',
|
|
455
|
+
title: '确认本次 workflow',
|
|
456
|
+
status: 'needs_input',
|
|
457
|
+
reasonCode: 'workflow-unconfirmed',
|
|
458
|
+
actionIds: actions.map((action) => action.id),
|
|
459
|
+
},
|
|
460
|
+
blockingReason: {
|
|
461
|
+
code: 'workflow-unconfirmed',
|
|
462
|
+
source: 'workflow',
|
|
463
|
+
severity: 'blocker',
|
|
464
|
+
message: 'change workflow is not confirmed',
|
|
465
|
+
},
|
|
466
|
+
availableActions: actions,
|
|
467
|
+
nextAction: `devflow flow confirm --slug=${slug}`,
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const review = (st.phases && st.phases.review) || {};
|
|
472
|
+
if (review.status === 'blocked') {
|
|
473
|
+
const actions = [
|
|
474
|
+
surfaceAction('continue-review', '继续 review 返工', `devflow review --slug=${slug} --round --continue-rounds`, { primary: true, kind: 'phase' }),
|
|
475
|
+
surfaceAction('force-pass-review', '带原因强制通过 review', `devflow review --slug=${slug} --force-pass --reason="..."`, {
|
|
476
|
+
kind: 'risk-acceptance',
|
|
477
|
+
danger: true,
|
|
478
|
+
requiresReason: true,
|
|
479
|
+
}),
|
|
480
|
+
];
|
|
481
|
+
return {
|
|
482
|
+
primaryPanel: {
|
|
483
|
+
type: 'review_blocked',
|
|
484
|
+
title: 'Review blocked',
|
|
485
|
+
status: 'needs_input',
|
|
486
|
+
reasonCode: 'review-blocked',
|
|
487
|
+
actionIds: actions.map((action) => action.id),
|
|
488
|
+
},
|
|
489
|
+
blockingReason: {
|
|
490
|
+
code: 'review-blocked',
|
|
491
|
+
source: 'review',
|
|
492
|
+
severity: 'blocker',
|
|
493
|
+
message: 'review is blocked by unresolved MUST findings',
|
|
494
|
+
},
|
|
495
|
+
availableActions: actions,
|
|
496
|
+
nextAction: actions[0].command,
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const pendingVerifyStart = checkpoint.latestPendingByType(st, 'verify_start');
|
|
501
|
+
if (pendingVerifyStart) {
|
|
502
|
+
const command = pendingVerifyStart.nextAction || `devflow checkpoint resolve --id=${pendingVerifyStart.id} --decision=start-verify`;
|
|
503
|
+
const actions = [
|
|
504
|
+
surfaceAction('start-verify', '允许进入 verify', command, { primary: true, kind: 'checkpoint-decision' }),
|
|
505
|
+
surfaceAction('show-checkpoint', '查看确认卡', `devflow checkpoint show --slug=${slug} --json`, { kind: 'open-surface' }),
|
|
506
|
+
];
|
|
507
|
+
return {
|
|
508
|
+
primaryPanel: {
|
|
509
|
+
type: 'verify_start',
|
|
510
|
+
title: '确认进入 verify',
|
|
511
|
+
status: 'needs_input',
|
|
512
|
+
reasonCode: 'pending-verify-start',
|
|
513
|
+
checkpointId: pendingVerifyStart.id,
|
|
514
|
+
actionIds: actions.map((action) => action.id),
|
|
515
|
+
},
|
|
516
|
+
blockingReason: {
|
|
517
|
+
code: 'pending-verify-start',
|
|
518
|
+
source: 'checkpoint',
|
|
519
|
+
severity: 'blocker',
|
|
520
|
+
message: pendingVerifyStart.summary || 'verify requires explicit confirmation',
|
|
521
|
+
},
|
|
522
|
+
availableActions: actions,
|
|
523
|
+
nextAction: command,
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
if (wf && wf.status === 'confirmed' && wf.currentStep) {
|
|
528
|
+
const command = workflowCommand(wf.currentStep, slug);
|
|
529
|
+
if (command) {
|
|
530
|
+
const actions = [surfaceAction('run-current-step', '运行当前 step', command, { primary: true, kind: 'phase' })];
|
|
531
|
+
return {
|
|
532
|
+
primaryPanel: {
|
|
533
|
+
type: 'current_step',
|
|
534
|
+
title: '继续当前 workflow step',
|
|
535
|
+
status: 'ready',
|
|
536
|
+
step: wf.currentStep,
|
|
537
|
+
actionIds: actions.map((action) => action.id),
|
|
538
|
+
},
|
|
539
|
+
blockingReason: null,
|
|
540
|
+
availableActions: actions,
|
|
541
|
+
nextAction: command,
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
if (review.status === 'completed') {
|
|
547
|
+
const actions = [surfaceAction('start-verify', '开始 verify', `devflow verify --slug=${slug}`, { primary: true, kind: 'phase' })];
|
|
548
|
+
return readyGuidance('verify_ready', '开始 verify', actions);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
const apply = (st.phases && st.phases.apply) || {};
|
|
552
|
+
const tasks = apply.tasks || [];
|
|
553
|
+
const openTasks = tasks.filter((task) => task.status !== 'done' && task.status !== 'skipped');
|
|
554
|
+
if (openTasks.length) {
|
|
555
|
+
const actions = [surfaceAction('continue-apply-task', '继续 apply task', `devflow apply --slug=${slug} --task=<id> --done`, { primary: true, kind: 'phase' })];
|
|
556
|
+
return readyGuidance('apply_tasks', '继续 apply task', actions);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
if (apply.status === 'completed') {
|
|
560
|
+
const actions = [surfaceAction('start-review', '开始 review', `devflow review --slug=${slug} --round`, { primary: true, kind: 'phase' })];
|
|
561
|
+
return readyGuidance('review_ready', '开始 review', actions);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
return {
|
|
565
|
+
primaryPanel: {
|
|
566
|
+
type: 'phase_hint',
|
|
567
|
+
title: '继续当前阶段',
|
|
568
|
+
status: 'ready',
|
|
569
|
+
phase: st.currentPhase || null,
|
|
570
|
+
actionIds: [],
|
|
571
|
+
},
|
|
572
|
+
blockingReason: null,
|
|
573
|
+
availableActions: [],
|
|
574
|
+
nextAction: null,
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function readyGuidance(type, title, actions) {
|
|
579
|
+
return {
|
|
580
|
+
primaryPanel: {
|
|
581
|
+
type,
|
|
582
|
+
title,
|
|
583
|
+
status: 'ready',
|
|
584
|
+
actionIds: actions.map((action) => action.id),
|
|
585
|
+
},
|
|
586
|
+
blockingReason: null,
|
|
587
|
+
availableActions: actions,
|
|
588
|
+
nextAction: actions[0] ? actions[0].command : null,
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function workflowCheckIssues(result) {
|
|
593
|
+
if (!result) return [];
|
|
594
|
+
return [...(result.errors || []), ...(result.warnings || [])];
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function workflowIssueActionLabel(issue) {
|
|
598
|
+
if (!issue) return '修复 workflow';
|
|
599
|
+
if (issue.code === 'missing-risk-step') return '按风险推荐补充 workflow';
|
|
600
|
+
if (issue.code === 'verify-below-level-default') return '恢复 verify 默认要求';
|
|
601
|
+
if (issue.code === 'pending-workflow-policy') return '查看 workflow 风险确认';
|
|
602
|
+
return '修复 workflow';
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function actionsFromConfirmationCard(card, kind) {
|
|
606
|
+
if (!card) return [];
|
|
607
|
+
return compactActions([
|
|
608
|
+
card.primaryAction ? actionFromCardAction(card.primaryAction, { primary: true, kind }) : null,
|
|
609
|
+
...((card.secondaryActions || []).map((action) => actionFromCardAction(action, { kind }))),
|
|
610
|
+
]);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function actionFromCardAction(action, options = {}) {
|
|
614
|
+
return surfaceAction(action.id, action.label || action.id, action.command || null, {
|
|
615
|
+
kind: options.kind || 'command',
|
|
616
|
+
primary: options.primary === true,
|
|
617
|
+
description: action.description || '',
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function actionFromCommand(id, label, command, options = {}) {
|
|
622
|
+
if (!command) return null;
|
|
623
|
+
return surfaceAction(id, label, command, options);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function surfaceAction(id, label, command, options = {}) {
|
|
627
|
+
return {
|
|
628
|
+
id,
|
|
629
|
+
label,
|
|
630
|
+
command,
|
|
631
|
+
kind: options.kind || 'command',
|
|
632
|
+
primary: options.primary === true,
|
|
633
|
+
danger: options.danger === true,
|
|
634
|
+
requiresReason: options.requiresReason === true,
|
|
635
|
+
description: options.description || '',
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function compactActions(actions) {
|
|
640
|
+
return actions.filter(Boolean);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function buildPhaseSurface(st) {
|
|
644
|
+
const items = state.PHASES.map((id) => {
|
|
645
|
+
const phase = (st.phases && st.phases[id]) || { status: 'pending' };
|
|
646
|
+
return {
|
|
647
|
+
id,
|
|
648
|
+
status: phase.status || 'pending',
|
|
649
|
+
ts: phase.ts || null,
|
|
650
|
+
current: st.currentPhase === id,
|
|
651
|
+
};
|
|
652
|
+
});
|
|
653
|
+
return {
|
|
654
|
+
current: items.find((item) => item.current) || null,
|
|
655
|
+
items,
|
|
656
|
+
summary: countStatuses(items),
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function buildArtifactSurface(cdir) {
|
|
661
|
+
return {
|
|
662
|
+
core: CORE_ARTIFACTS.map((name) => artifactInfo(cdir, name)),
|
|
663
|
+
optional: OPTIONAL_ARTIFACTS.map((name) => artifactInfo(cdir, name)),
|
|
664
|
+
directories: {
|
|
665
|
+
delta: directoryInfo(cdir, 'delta'),
|
|
666
|
+
reports: directoryInfo(cdir, 'reports'),
|
|
667
|
+
},
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function artifactInfo(cdir, name) {
|
|
672
|
+
return {
|
|
673
|
+
name,
|
|
674
|
+
path: name,
|
|
675
|
+
exists: fsSync.existsSync(path.join(cdir, name)),
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
function directoryInfo(cdir, name) {
|
|
680
|
+
const dir = path.join(cdir, name);
|
|
681
|
+
const files = fsSync.existsSync(dir)
|
|
682
|
+
? fsSync.readdirSync(dir).filter((file) => file.endsWith('.md')).sort()
|
|
683
|
+
: [];
|
|
684
|
+
return { path: `${name}/`, exists: fsSync.existsSync(dir), count: files.length, files };
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function buildApplySurface(st) {
|
|
688
|
+
const apply = (st.phases && st.phases.apply) || {};
|
|
689
|
+
const tasks = (apply.tasks || []).map((task) => ({
|
|
690
|
+
id: task.id,
|
|
691
|
+
title: task.title || '',
|
|
692
|
+
status: task.status || 'pending',
|
|
693
|
+
branch: task.branch || null,
|
|
694
|
+
worktree: task.worktree || null,
|
|
695
|
+
notesCount: (task.notes || []).length,
|
|
696
|
+
}));
|
|
697
|
+
const summary = {
|
|
698
|
+
total: tasks.length,
|
|
699
|
+
done: tasks.filter((task) => task.status === 'done').length,
|
|
700
|
+
inProgress: tasks.filter((task) => task.status === 'in_progress').length,
|
|
701
|
+
failed: tasks.filter((task) => task.status === 'failed').length,
|
|
702
|
+
open: tasks.filter((task) => task.status !== 'done' && task.status !== 'skipped').length,
|
|
703
|
+
};
|
|
704
|
+
return {
|
|
705
|
+
status: apply.status || 'pending',
|
|
706
|
+
currentTask: apply.currentTask || null,
|
|
707
|
+
iterations: (st.iterations && st.iterations.apply) || 0,
|
|
708
|
+
tasks,
|
|
709
|
+
summary,
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
function buildReviewSurface(st) {
|
|
714
|
+
const review = (st.phases && st.phases.review) || {};
|
|
715
|
+
const rounds = review.rounds || [];
|
|
716
|
+
return {
|
|
717
|
+
status: review.status || 'pending',
|
|
718
|
+
maxRounds: review.max_rounds || review.maxRounds || 3,
|
|
719
|
+
rounds: rounds.map((round) => ({
|
|
720
|
+
round: round.round,
|
|
721
|
+
outcome: round.outcome || null,
|
|
722
|
+
must: round.must || 0,
|
|
723
|
+
should: round.should || 0,
|
|
724
|
+
nit: round.nit || 0,
|
|
725
|
+
source: round.source || null,
|
|
726
|
+
reason: round.reason || null,
|
|
727
|
+
})),
|
|
728
|
+
latestRound: rounds.length ? rounds[rounds.length - 1] : null,
|
|
729
|
+
mustTotal: sumRounds(rounds, 'must'),
|
|
730
|
+
shouldTotal: sumRounds(rounds, 'should'),
|
|
731
|
+
nitTotal: sumRounds(rounds, 'nit'),
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
function buildVerifySurface(st, cdir) {
|
|
736
|
+
const verify = (st.phases && st.phases.verify) || {};
|
|
737
|
+
const reportsDir = path.join(cdir, 'reports');
|
|
738
|
+
const requiredReports = workflowVerify.requiredReportsForState(st);
|
|
739
|
+
const reports = listReports(reportsDir);
|
|
740
|
+
const presentReports = [...new Set(reports.map((report) => report.name).filter(Boolean))].sort();
|
|
741
|
+
return {
|
|
742
|
+
status: verify.status || 'pending',
|
|
743
|
+
requiredReports,
|
|
744
|
+
presentReports,
|
|
745
|
+
missingReports: requiredReports.filter((name) => !presentReports.includes(name)),
|
|
746
|
+
reports,
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
function listReports(reportsDir) {
|
|
751
|
+
const out = [];
|
|
752
|
+
const seen = new Set();
|
|
753
|
+
const aggregateReport = aggregate.readAggregate(reportsDir);
|
|
754
|
+
for (const report of aggregateReport.reports || []) {
|
|
755
|
+
const name = reportNameFromKind(report.kind);
|
|
756
|
+
seen.add(name);
|
|
757
|
+
out.push({
|
|
758
|
+
kind: report.kind,
|
|
759
|
+
name,
|
|
760
|
+
status: report.status || 'pending',
|
|
761
|
+
total: report.total,
|
|
762
|
+
passed: report.passed,
|
|
763
|
+
failed: report.failed,
|
|
764
|
+
failureType: report.failureType || null,
|
|
765
|
+
file: report.file || 'test-report.md',
|
|
766
|
+
section: report.section || report.kind,
|
|
767
|
+
ts: report.ts || null,
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
let files = [];
|
|
771
|
+
try { files = fsSync.readdirSync(reportsDir).filter((file) => file.endsWith('.md')).sort(); } catch (_) { files = []; }
|
|
772
|
+
for (const file of files) {
|
|
773
|
+
if (file === 'test-report.md' || seen.has(file)) continue;
|
|
774
|
+
out.push({
|
|
775
|
+
kind: workflowVerify.reportKindFromName(file),
|
|
776
|
+
name: file,
|
|
777
|
+
status: readReportStatus(path.join(reportsDir, file)),
|
|
778
|
+
total: null,
|
|
779
|
+
passed: null,
|
|
780
|
+
failed: null,
|
|
781
|
+
failureType: null,
|
|
782
|
+
file,
|
|
783
|
+
section: null,
|
|
784
|
+
ts: null,
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
return out;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function reportNameFromKind(kind) {
|
|
791
|
+
return kind === 'self-test' ? 'self-test.md' : `${kind}-test.md`;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
function readReportStatus(file) {
|
|
795
|
+
let text = '';
|
|
796
|
+
try { text = fsSync.readFileSync(file, 'utf8'); } catch (_) { return 'pending'; }
|
|
797
|
+
const match = text.match(/^status:\s*(.+)$/m);
|
|
798
|
+
return match ? match[1].trim() : 'pending';
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function buildRiskSignalSurface(st) {
|
|
802
|
+
const items = (st.riskSignals || []).map((risk) => ({
|
|
803
|
+
type: risk.type,
|
|
804
|
+
status: risk.status || 'open',
|
|
805
|
+
reason: risk.reason || '',
|
|
806
|
+
source: risk.source || null,
|
|
807
|
+
evidence: risk.evidence || [],
|
|
808
|
+
createdAt: risk.createdAt || null,
|
|
809
|
+
updatedAt: risk.updatedAt || null,
|
|
810
|
+
}));
|
|
811
|
+
return { items, summary: countStatuses(items) };
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
function countStatuses(items) {
|
|
815
|
+
const summary = { total: items.length };
|
|
816
|
+
for (const item of items) {
|
|
817
|
+
const key = statusSummaryKey(item.status || 'pending');
|
|
818
|
+
summary[key] = (summary[key] || 0) + 1;
|
|
819
|
+
}
|
|
820
|
+
return summary;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
function statusSummaryKey(status) {
|
|
824
|
+
if (status === 'in_progress') return 'inProgress';
|
|
825
|
+
return String(status || 'pending').replace(/_([a-z])/g, (_, ch) => ch.toUpperCase());
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
function sumRounds(rounds, field) {
|
|
829
|
+
return rounds.reduce((sum, round) => sum + (Number(round[field]) || 0), 0);
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
function checkpointCardOptions(cp) {
|
|
833
|
+
if (cp.type === 'workflow_policy') {
|
|
834
|
+
return {
|
|
835
|
+
type: 'workflow_policy_confirm',
|
|
836
|
+
title: '确认 workflow 风险',
|
|
837
|
+
primaryActionId: 'accept-risk',
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
return {};
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
function statusNextAction(slug, st, workflowCheckResult, pending) {
|
|
844
|
+
if (pending) return `devflow checkpoint show --slug=${slug} --json`;
|
|
845
|
+
if (workflowCheckResult && workflowCheckResult.nextAction) return workflowCheckResult.nextAction;
|
|
846
|
+
if (st.workflow && st.workflow.status === 'confirmed' && st.workflow.currentStep) {
|
|
847
|
+
const cmd = workflowCommand(st.workflow.currentStep, slug);
|
|
848
|
+
if (cmd) return cmd;
|
|
849
|
+
}
|
|
850
|
+
return null;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
function hintNext(st, colors, slug, workflowCheck = null) {
|
|
854
|
+
if (workflowCheck && workflowCheck.nextAction) {
|
|
855
|
+
log.raw(colors.dim('next: ') + colors.bold(workflowCheck.nextAction));
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
if (st.workflow && st.workflow.status === 'confirmed' && st.workflow.currentStep) {
|
|
859
|
+
const cmd = workflowCommand(st.workflow.currentStep, slug);
|
|
860
|
+
if (cmd) {
|
|
861
|
+
log.raw(colors.dim('next: ') + colors.bold(cmd));
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
const apply = (st.phases && st.phases.apply) || {};
|
|
866
|
+
const review = (st.phases && st.phases.review) || {};
|
|
867
|
+
const pendingVerifyStart = checkpoint.latestPendingByType(st, 'verify_start');
|
|
868
|
+
const tasks = apply.tasks || [];
|
|
869
|
+
const openTasks = tasks.filter(t => t.status !== 'done' && t.status !== 'skipped');
|
|
870
|
+
if (review.status === 'blocked') {
|
|
871
|
+
log.raw(colors.dim('next: ') + 'fix MUSTs & ' + colors.bold(`devflow review --slug=${slug} --round --continue-rounds`) + ' OR ' + colors.bold(`devflow review --slug=${slug} --force-pass --reason="..."`));
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
if (pendingVerifyStart) {
|
|
875
|
+
log.raw(colors.dim('next: ') + colors.bold(pendingVerifyStart.nextAction || `devflow checkpoint resolve --id=${pendingVerifyStart.id} --decision=start-verify`));
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
if (review.status === 'completed') {
|
|
879
|
+
log.raw(colors.dim('next: ') + colors.bold(`devflow verify --slug=${slug}`));
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
if (openTasks.length) {
|
|
883
|
+
log.raw(colors.dim('next: ') + `continue ${openTasks.length} open task${openTasks.length===1?'':'s'} — ` + colors.bold(`devflow apply --slug=${slug} --task=<id> --done`));
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
if (apply.status === 'completed') {
|
|
887
|
+
log.raw(colors.dim('next: ') + colors.bold(`devflow review --slug=${slug} --round`));
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
log.raw(colors.dim('next: ') + 'follow the current phase. see ' + colors.bold('devflow help') + ' for commands.');
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
function nextWorkflowStep(wf) {
|
|
894
|
+
const steps = wf.steps || [];
|
|
895
|
+
const currentIdx = steps.findIndex((s) => s.id === wf.currentStep);
|
|
896
|
+
for (let i = currentIdx + 1; i < steps.length; i += 1) {
|
|
897
|
+
if (steps[i].status !== 'disabled' && steps[i].status !== 'completed') return steps[i].id;
|
|
898
|
+
}
|
|
899
|
+
return null;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
function summarizeWorkflowOverrides(wf) {
|
|
903
|
+
return (wf.overrides || []).map((item) => {
|
|
904
|
+
if (item.type === 'add-step') return `add-step ${item.step}`;
|
|
905
|
+
if (item.type === 'disable-step') return `disable-step ${item.step}`;
|
|
906
|
+
if (item.type === 'move-step') return `move-step ${item.step}`;
|
|
907
|
+
return `${item.type || 'override'} ${item.step || ''}`.trim();
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
function workflowCommand(step, slug) {
|
|
912
|
+
const commands = {
|
|
913
|
+
requirement: `devflow requirement --slug=${slug}`,
|
|
914
|
+
design: `devflow design --slug=${slug}`,
|
|
915
|
+
plan: `devflow plan --slug=${slug}`,
|
|
916
|
+
apply: `devflow apply --slug=${slug}`,
|
|
917
|
+
review: `devflow review --slug=${slug} --round`,
|
|
918
|
+
verify: `devflow verify --slug=${slug}`,
|
|
919
|
+
deliver: `devflow deliver --slug=${slug}`,
|
|
920
|
+
archive: `devflow archive --slug=${slug}`,
|
|
921
|
+
};
|
|
922
|
+
return commands[step] || null;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
module.exports = { run };
|