@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,88 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const DEFAULT_REQUIRED_BY_LEVEL = {
|
|
4
|
+
L0: ['unit-test.md'],
|
|
5
|
+
L1: ['unit-test.md', 'smoke-test.md'],
|
|
6
|
+
L2: ['unit-test.md', 'integration-test.md', 'smoke-test.md', 'self-test.md'],
|
|
7
|
+
L3: ['unit-test.md', 'integration-test.md', 'e2e-test.md', 'smoke-test.md', 'self-test.md'],
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const REPORT_ALIASES = {
|
|
11
|
+
unit: 'unit-test.md',
|
|
12
|
+
'unit-test': 'unit-test.md',
|
|
13
|
+
integration: 'integration-test.md',
|
|
14
|
+
'integration-test': 'integration-test.md',
|
|
15
|
+
e2e: 'e2e-test.md',
|
|
16
|
+
'e2e-test': 'e2e-test.md',
|
|
17
|
+
smoke: 'smoke-test.md',
|
|
18
|
+
'smoke-test': 'smoke-test.md',
|
|
19
|
+
self: 'self-test.md',
|
|
20
|
+
'self-test': 'self-test.md',
|
|
21
|
+
self_test: 'self-test.md',
|
|
22
|
+
regression: 'regression-test.md',
|
|
23
|
+
'regression-test': 'regression-test.md',
|
|
24
|
+
perf: 'perf-test.md',
|
|
25
|
+
'perf-test': 'perf-test.md',
|
|
26
|
+
contract: 'contract-test.md',
|
|
27
|
+
'contract-test': 'contract-test.md',
|
|
28
|
+
joint: 'joint-test.md',
|
|
29
|
+
'joint-test': 'joint-test.md',
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function normalizeReportList(input) {
|
|
33
|
+
const parts = Array.isArray(input)
|
|
34
|
+
? input
|
|
35
|
+
: String(input || '').split(',');
|
|
36
|
+
const out = [];
|
|
37
|
+
const seen = new Set();
|
|
38
|
+
for (const raw of parts) {
|
|
39
|
+
const normalized = normalizeReportName(raw);
|
|
40
|
+
if (!seen.has(normalized)) {
|
|
41
|
+
out.push(normalized);
|
|
42
|
+
seen.add(normalized);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (!out.length) throw new Error('at least one verify report is required');
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function normalizeReportName(raw) {
|
|
50
|
+
const value = String(raw || '').trim();
|
|
51
|
+
if (!value) throw new Error('empty verify report name');
|
|
52
|
+
const key = value.replace(/^reports\//, '').replace(/^test-report\.md#/, '').toLowerCase();
|
|
53
|
+
if (REPORT_ALIASES[key]) return REPORT_ALIASES[key];
|
|
54
|
+
if (/^[a-z0-9][a-z0-9-]*\.md$/.test(key)) return key;
|
|
55
|
+
throw new Error(`unknown verify report: ${value}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function defaultRequiredReports(level) {
|
|
59
|
+
return (DEFAULT_REQUIRED_BY_LEVEL[level] || DEFAULT_REQUIRED_BY_LEVEL.L2).slice();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function requiredReportsForState(st) {
|
|
63
|
+
const override = st && st.workflow && st.workflow.verify && st.workflow.verify.requiredReports;
|
|
64
|
+
if (Array.isArray(override) && override.length) return normalizeReportList(override);
|
|
65
|
+
return defaultRequiredReports(st && st.level);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function missingDefaultReports(level, reports) {
|
|
69
|
+
const selected = new Set(normalizeReportList(reports));
|
|
70
|
+
return defaultRequiredReports(level).filter((name) => !selected.has(name));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function reportKindFromName(name) {
|
|
74
|
+
const normalized = normalizeReportName(name);
|
|
75
|
+
if (normalized === 'self-test.md') return 'self-test';
|
|
76
|
+
return normalized.replace(/-test\.md$/, '').replace(/\.md$/, '');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
module.exports = {
|
|
80
|
+
DEFAULT_REQUIRED_BY_LEVEL,
|
|
81
|
+
REPORT_ALIASES,
|
|
82
|
+
normalizeReportList,
|
|
83
|
+
normalizeReportName,
|
|
84
|
+
defaultRequiredReports,
|
|
85
|
+
requiredReportsForState,
|
|
86
|
+
missingDefaultReports,
|
|
87
|
+
reportKindFromName,
|
|
88
|
+
};
|
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fsSync = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const PROTECTED_GATES = ['review', 'verify', 'deliver'];
|
|
7
|
+
|
|
8
|
+
const BUILTIN_RECIPES = {
|
|
9
|
+
micro: {
|
|
10
|
+
label: '极简改动',
|
|
11
|
+
steps: [
|
|
12
|
+
{ id: 'plan', skill: 'plan' },
|
|
13
|
+
{ id: 'apply', skill: 'apply' },
|
|
14
|
+
gate('review', 'code-review'),
|
|
15
|
+
gate('verify', 'verify'),
|
|
16
|
+
gate('deliver', 'deliver'),
|
|
17
|
+
],
|
|
18
|
+
},
|
|
19
|
+
standard: {
|
|
20
|
+
label: '标准研发',
|
|
21
|
+
steps: [
|
|
22
|
+
{ id: 'requirement', skill: 'requirement-analysis' },
|
|
23
|
+
{ id: 'design', skill: 'tech-spec' },
|
|
24
|
+
{ id: 'plan', skill: 'plan' },
|
|
25
|
+
{ id: 'apply', skill: 'apply' },
|
|
26
|
+
gate('review', 'code-review'),
|
|
27
|
+
gate('verify', 'verify'),
|
|
28
|
+
gate('deliver', 'deliver'),
|
|
29
|
+
{ id: 'archive', skill: 'archive', optional: true },
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
'dependency-upgrade': {
|
|
33
|
+
label: '依赖升级',
|
|
34
|
+
steps: [
|
|
35
|
+
{ id: 'dependency-upgrade', skill: 'dependency-upgrade' },
|
|
36
|
+
{ id: 'plan', skill: 'plan' },
|
|
37
|
+
{ id: 'apply', skill: 'apply' },
|
|
38
|
+
gate('review', 'code-review'),
|
|
39
|
+
gate('verify', 'verify'),
|
|
40
|
+
gate('deliver', 'deliver'),
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
'ci-fix': {
|
|
44
|
+
label: 'CI 修复',
|
|
45
|
+
steps: [
|
|
46
|
+
{ id: 'ci-fix', skill: 'ci-fix' },
|
|
47
|
+
{ id: 'apply', skill: 'apply' },
|
|
48
|
+
gate('review', 'code-review'),
|
|
49
|
+
gate('verify', 'verify'),
|
|
50
|
+
gate('deliver', 'deliver'),
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
'frontend-quality': {
|
|
54
|
+
label: '前端改动',
|
|
55
|
+
extends: 'standard',
|
|
56
|
+
insertAfter: {
|
|
57
|
+
apply: [{ id: 'frontend-quality', skill: 'frontend-quality', optional: true }],
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
'security-hardening': {
|
|
61
|
+
label: '安全敏感改动',
|
|
62
|
+
extends: 'standard',
|
|
63
|
+
insertAfter: {
|
|
64
|
+
apply: [{ id: 'security-hardening', skill: 'security-hardening', optional: true }],
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
refactor: {
|
|
68
|
+
label: '技术重构',
|
|
69
|
+
steps: [
|
|
70
|
+
{ id: 'requirement', skill: 'requirement-analysis', optional: true },
|
|
71
|
+
{ id: 'design', skill: 'tech-spec' },
|
|
72
|
+
{ id: 'plan', skill: 'plan' },
|
|
73
|
+
{ id: 'apply', skill: 'apply' },
|
|
74
|
+
gate('review', 'code-review'),
|
|
75
|
+
gate('verify', 'verify'),
|
|
76
|
+
gate('deliver', 'deliver'),
|
|
77
|
+
],
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
function gate(id, skill) {
|
|
82
|
+
return { id, skill, required: true, protected: true };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function recommendRecipe(state, cfg = {}) {
|
|
86
|
+
if (state.mode === 'micro' || state.level === 'L0') {
|
|
87
|
+
return { id: 'micro', reason: 'change is L0/micro' };
|
|
88
|
+
}
|
|
89
|
+
const risks = new Set((state.riskSignals || []).filter((r) => (r.status || 'open') === 'open').map((r) => r.type));
|
|
90
|
+
if (risks.has('dependency_change')) return { id: 'dependency-upgrade', reason: 'open dependency_change risk signal' };
|
|
91
|
+
if (risks.has('ci_failure')) return { id: 'ci-fix', reason: 'open ci_failure risk signal' };
|
|
92
|
+
if (risks.has('frontend_change')) return { id: 'frontend-quality', reason: 'open frontend_change risk signal' };
|
|
93
|
+
if (risks.has('security_sensitive')) return { id: 'security-hardening', reason: 'open security_sensitive risk signal' };
|
|
94
|
+
const configured = cfg.defaults && cfg.defaults.workflow;
|
|
95
|
+
if (configured) return { id: configured, reason: 'project default workflow' };
|
|
96
|
+
return { id: 'standard', reason: 'default workflow' };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function expandRecipe(id, cfg = {}) {
|
|
100
|
+
const recipes = (cfg.workflows && cfg.workflows.recipes) || {};
|
|
101
|
+
const recipe = recipes[id] || BUILTIN_RECIPES[id];
|
|
102
|
+
if (!recipe) throw new Error(`unknown workflow recipe: ${id}`);
|
|
103
|
+
|
|
104
|
+
const source = recipes[id] ? 'project' : 'builtin';
|
|
105
|
+
let steps;
|
|
106
|
+
if (recipe.steps) {
|
|
107
|
+
steps = clone(recipe.steps).map((s) => normalizeStep(s, source));
|
|
108
|
+
} else if (recipe.extends) {
|
|
109
|
+
steps = expandRecipe(recipe.extends, cfg).steps.map((s) => ({ ...s }));
|
|
110
|
+
} else {
|
|
111
|
+
throw new Error(`workflow recipe has no steps or extends: ${id}`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
steps = applyOverrides(steps, recipe, source);
|
|
115
|
+
validateSteps(steps, { recipeId: id });
|
|
116
|
+
return {
|
|
117
|
+
id,
|
|
118
|
+
label: recipe.label || id,
|
|
119
|
+
description: recipe.description || '',
|
|
120
|
+
source,
|
|
121
|
+
steps,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function applyOverrides(steps, recipe, source) {
|
|
126
|
+
let out = steps.slice();
|
|
127
|
+
if (recipe.replaceStep) {
|
|
128
|
+
for (const [id, replacement] of Object.entries(recipe.replaceStep)) {
|
|
129
|
+
const idx = findStepIndex(out, id);
|
|
130
|
+
out[idx] = normalizeStep({ ...replacement, id: replacement.id || id }, source);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (recipe.disableStep) {
|
|
134
|
+
for (const id of asList(recipe.disableStep)) {
|
|
135
|
+
const idx = findStepIndex(out, id);
|
|
136
|
+
if (isProtected(out[idx])) throw new Error(`cannot disable protected workflow gate: ${id}`);
|
|
137
|
+
out[idx] = { ...out[idx], status: 'disabled' };
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
out = insertAll(out, recipe.insertBefore, source, 'before');
|
|
141
|
+
out = insertAll(out, recipe.insertAfter, source, 'after');
|
|
142
|
+
return out;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function insertAll(steps, spec, source, mode) {
|
|
146
|
+
let out = steps.slice();
|
|
147
|
+
for (const [anchor, inserts] of Object.entries(spec || {})) {
|
|
148
|
+
let anchorIdx = findStepIndex(out, anchor);
|
|
149
|
+
const normalized = asList(inserts).map((s) => normalizeStep(s, source));
|
|
150
|
+
const at = mode === 'before' ? anchorIdx : anchorIdx + 1;
|
|
151
|
+
out.splice(at, 0, ...normalized);
|
|
152
|
+
}
|
|
153
|
+
return out;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function validateSteps(steps, { recipeId = 'workflow' } = {}) {
|
|
157
|
+
const seen = new Set();
|
|
158
|
+
for (const step of steps) {
|
|
159
|
+
if (!step.id) throw new Error(`${recipeId}: workflow step id is required`);
|
|
160
|
+
if (seen.has(step.id)) throw new Error(`${recipeId}: duplicate workflow step id: ${step.id}`);
|
|
161
|
+
seen.add(step.id);
|
|
162
|
+
}
|
|
163
|
+
for (const gateId of PROTECTED_GATES) {
|
|
164
|
+
const step = steps.find((s) => s.id === gateId);
|
|
165
|
+
if (!step) throw new Error(`${recipeId}: missing protected workflow gate: ${gateId}`);
|
|
166
|
+
if (!isProtected(step)) throw new Error(`${recipeId}: protected workflow gate must be required: ${gateId}`);
|
|
167
|
+
}
|
|
168
|
+
const order = steps.map((s) => s.id);
|
|
169
|
+
if (order.indexOf('review') > order.indexOf('verify')) throw new Error(`${recipeId}: verify must stay after review`);
|
|
170
|
+
if (order.indexOf('verify') > order.indexOf('deliver')) throw new Error(`${recipeId}: deliver must stay after verify`);
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function createDraft({ recipe, state, reason = '' }) {
|
|
175
|
+
const now = new Date().toISOString();
|
|
176
|
+
return {
|
|
177
|
+
status: 'draft',
|
|
178
|
+
baseRecipe: {
|
|
179
|
+
id: recipe.id,
|
|
180
|
+
label: recipe.label,
|
|
181
|
+
source: recipe.source,
|
|
182
|
+
reason,
|
|
183
|
+
},
|
|
184
|
+
overrides: [],
|
|
185
|
+
baseSteps: clone(recipe.steps),
|
|
186
|
+
steps: clone(recipe.steps),
|
|
187
|
+
currentStep: firstRunnableStep(recipe.steps),
|
|
188
|
+
audit: [{ ts: now, event: 'draft.create', recipe: recipe.id, reason }],
|
|
189
|
+
draftedAt: now,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function addStep(workflow, { skill, id, after, before }) {
|
|
194
|
+
ensureWorkflow(workflow);
|
|
195
|
+
if (!skill) throw new Error('skill is required');
|
|
196
|
+
const step = normalizeStep({ id: id || skill, skill, optional: true, source: 'change' }, 'change');
|
|
197
|
+
if (workflow.steps.some((s) => s.id === step.id)) throw new Error(`workflow step already exists: ${step.id}`);
|
|
198
|
+
const anchor = after || before;
|
|
199
|
+
if (!anchor) throw new Error('add-step requires --after=<step> or --before=<step>');
|
|
200
|
+
const idx = findStepIndex(workflow.steps, anchor);
|
|
201
|
+
const at = before ? idx : idx + 1;
|
|
202
|
+
workflow.steps.splice(at, 0, step);
|
|
203
|
+
pushOverride(workflow, { type: 'add-step', step: step.id, skill, after: after || null, before: before || null });
|
|
204
|
+
audit(workflow, 'step.add', { step: step.id, skill, after: after || null, before: before || null });
|
|
205
|
+
return step;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function disableStep(workflow, { stepId, reason, allowRequired = false }) {
|
|
209
|
+
ensureWorkflow(workflow);
|
|
210
|
+
const step = findStep(workflow.steps, stepId);
|
|
211
|
+
if ((isProtected(step) || step.required) && !allowRequired) {
|
|
212
|
+
const e = new Error(`cannot disable required workflow step without checkpoint: ${stepId}`);
|
|
213
|
+
e.code = 'WORKFLOW_POLICY';
|
|
214
|
+
throw e;
|
|
215
|
+
}
|
|
216
|
+
if (!reason) throw new Error('disable-step requires --reason="..."');
|
|
217
|
+
step.status = 'disabled';
|
|
218
|
+
step.disabledReason = reason;
|
|
219
|
+
pushOverride(workflow, { type: 'disable-step', step: stepId, reason });
|
|
220
|
+
audit(workflow, 'step.disable', { step: stepId, reason });
|
|
221
|
+
return step;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function moveStep(workflow, { stepId, after, before }) {
|
|
225
|
+
ensureWorkflow(workflow);
|
|
226
|
+
const anchor = after || before;
|
|
227
|
+
if (!anchor) throw new Error('move-step requires --after=<step> or --before=<step>');
|
|
228
|
+
const step = findStep(workflow.steps, stepId);
|
|
229
|
+
if (isProtected(step)) {
|
|
230
|
+
const reviewIdx = findStepIndex(workflow.steps, 'review');
|
|
231
|
+
const anchorIdx = findStepIndex(workflow.steps, anchor);
|
|
232
|
+
const targetIdx = before ? anchorIdx : anchorIdx + 1;
|
|
233
|
+
if (step.id !== 'review' && targetIdx <= reviewIdx) {
|
|
234
|
+
const e = new Error(`cannot move protected workflow gate before review: ${stepId}`);
|
|
235
|
+
e.code = 'WORKFLOW_POLICY';
|
|
236
|
+
throw e;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
const from = findStepIndex(workflow.steps, stepId);
|
|
240
|
+
const [removed] = workflow.steps.splice(from, 1);
|
|
241
|
+
let anchorIdx = findStepIndex(workflow.steps, anchor);
|
|
242
|
+
const at = before ? anchorIdx : anchorIdx + 1;
|
|
243
|
+
workflow.steps.splice(at, 0, removed);
|
|
244
|
+
validateSteps(workflow.steps, { recipeId: 'change workflow' });
|
|
245
|
+
pushOverride(workflow, { type: 'move-step', step: stepId, after: after || null, before: before || null });
|
|
246
|
+
audit(workflow, 'step.move', { step: stepId, after: after || null, before: before || null });
|
|
247
|
+
return removed;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function setVerifyReports(workflow, { requiredReports, reason }) {
|
|
251
|
+
ensureWorkflow(workflow);
|
|
252
|
+
workflow.verify = workflow.verify || {};
|
|
253
|
+
workflow.verify.requiredReports = requiredReports.slice();
|
|
254
|
+
if (reason) workflow.verify.reason = reason;
|
|
255
|
+
pushOverride(workflow, { type: 'set-verify', requiredReports: requiredReports.slice(), reason: reason || null });
|
|
256
|
+
audit(workflow, 'verify.set-reports', { requiredReports: requiredReports.slice(), reason: reason || null });
|
|
257
|
+
return workflow.verify;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function confirmWorkflow(workflow) {
|
|
261
|
+
ensureWorkflow(workflow);
|
|
262
|
+
workflow.status = 'confirmed';
|
|
263
|
+
workflow.confirmedAt = new Date().toISOString();
|
|
264
|
+
workflow.currentStep = firstRunnableStep(workflow.steps);
|
|
265
|
+
audit(workflow, 'workflow.confirm', { currentStep: workflow.currentStep });
|
|
266
|
+
return workflow;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function assertStepAllowed(workflow, stepId) {
|
|
270
|
+
if (!workflow) return true;
|
|
271
|
+
ensureWorkflow(workflow);
|
|
272
|
+
if (workflow.status !== 'confirmed') {
|
|
273
|
+
const e = new Error('workflow draft is not confirmed');
|
|
274
|
+
e.code = 'WORKFLOW_NOT_CONFIRMED';
|
|
275
|
+
e.currentStep = workflow.currentStep || firstRunnableStep(workflow.steps);
|
|
276
|
+
throw e;
|
|
277
|
+
}
|
|
278
|
+
const current = currentRunnableStep(workflow);
|
|
279
|
+
if (!current) return true;
|
|
280
|
+
if (current.id !== stepId) {
|
|
281
|
+
const e = new Error(`workflow current step is ${current.id}, not ${stepId}`);
|
|
282
|
+
e.code = 'WORKFLOW_STEP_BLOCKED';
|
|
283
|
+
e.currentStep = current.id;
|
|
284
|
+
e.requestedStep = stepId;
|
|
285
|
+
throw e;
|
|
286
|
+
}
|
|
287
|
+
return true;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function completeStep(workflow, stepId) {
|
|
291
|
+
if (!workflow || workflow.status !== 'confirmed') return false;
|
|
292
|
+
ensureWorkflow(workflow);
|
|
293
|
+
const idx = findStepIndex(workflow.steps, stepId);
|
|
294
|
+
const step = workflow.steps[idx];
|
|
295
|
+
if (step.status === 'disabled') return false;
|
|
296
|
+
step.status = 'completed';
|
|
297
|
+
step.completedAt = new Date().toISOString();
|
|
298
|
+
const next = nextRunnableStep(workflow.steps, idx + 1);
|
|
299
|
+
workflow.currentStep = next ? next.id : null;
|
|
300
|
+
audit(workflow, 'step.complete', { step: stepId, nextStep: workflow.currentStep });
|
|
301
|
+
return true;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function diffWorkflow(workflow) {
|
|
305
|
+
ensureWorkflow(workflow);
|
|
306
|
+
const base = new Set((workflow.baseSteps || []).map((s) => s.id));
|
|
307
|
+
const added = [];
|
|
308
|
+
const disabled = [];
|
|
309
|
+
const moved = [];
|
|
310
|
+
for (const step of workflow.steps || []) {
|
|
311
|
+
if (!base.has(step.id)) added.push(step.id);
|
|
312
|
+
if (step.status === 'disabled') disabled.push(step.id);
|
|
313
|
+
}
|
|
314
|
+
for (const item of workflow.overrides || []) {
|
|
315
|
+
if (item.type === 'move-step' && !moved.includes(item.step)) {
|
|
316
|
+
moved.push(item.step);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
const verifyReports = workflow.verify && Array.isArray(workflow.verify.requiredReports)
|
|
320
|
+
? workflow.verify.requiredReports.slice()
|
|
321
|
+
: [];
|
|
322
|
+
return { added, disabled, moved, verifyReports };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function installedSkillExists(root, skill) {
|
|
326
|
+
if (BUILTIN_STEP_SKILLS.has(skill)) return true;
|
|
327
|
+
return fsSync.existsSync(path.join(root, 'skills', skill, 'SKILL.md'))
|
|
328
|
+
|| fsSync.existsSync(path.join(__dirname, '..', '..', 'skills', skill, 'SKILL.md'));
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const BUILTIN_STEP_SKILLS = new Set([
|
|
332
|
+
'apply',
|
|
333
|
+
'archive',
|
|
334
|
+
'code-review',
|
|
335
|
+
'deliver',
|
|
336
|
+
'plan',
|
|
337
|
+
'requirement-analysis',
|
|
338
|
+
'review',
|
|
339
|
+
'tech-spec',
|
|
340
|
+
'verify',
|
|
341
|
+
]);
|
|
342
|
+
|
|
343
|
+
function normalizeStep(input, source) {
|
|
344
|
+
const id = input.id || input.skill;
|
|
345
|
+
return {
|
|
346
|
+
id,
|
|
347
|
+
skill: input.skill || id,
|
|
348
|
+
label: input.label || id,
|
|
349
|
+
required: input.required === true || PROTECTED_GATES.includes(id),
|
|
350
|
+
optional: input.optional === true,
|
|
351
|
+
protected: input.protected === true || PROTECTED_GATES.includes(id),
|
|
352
|
+
source: input.source || source,
|
|
353
|
+
status: input.status || 'pending',
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function firstRunnableStep(steps) {
|
|
358
|
+
const step = (steps || []).find((s) => s.status !== 'disabled');
|
|
359
|
+
return step ? step.id : null;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function currentRunnableStep(workflow) {
|
|
363
|
+
const steps = workflow.steps || [];
|
|
364
|
+
const current = workflow.currentStep ? steps.find((s) => s.id === workflow.currentStep) : null;
|
|
365
|
+
if (current && current.status !== 'disabled' && current.status !== 'completed') return current;
|
|
366
|
+
const start = current ? steps.indexOf(current) + 1 : 0;
|
|
367
|
+
return nextRunnableStep(steps, start);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function nextRunnableStep(steps, start = 0) {
|
|
371
|
+
for (let i = start; i < (steps || []).length; i += 1) {
|
|
372
|
+
const step = steps[i];
|
|
373
|
+
if (step.status !== 'disabled' && step.status !== 'completed') return step;
|
|
374
|
+
}
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function isProtected(step) {
|
|
379
|
+
return !!(step && (step.protected || PROTECTED_GATES.includes(step.id)));
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function ensureWorkflow(workflow) {
|
|
383
|
+
if (!workflow || !Array.isArray(workflow.steps)) throw new Error('workflow draft not found. run devflow flow draft first.');
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function findStep(steps, id) {
|
|
387
|
+
const step = steps.find((s) => s.id === id);
|
|
388
|
+
if (!step) throw new Error(`workflow step not found: ${id}`);
|
|
389
|
+
return step;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function findStepIndex(steps, id) {
|
|
393
|
+
const idx = steps.findIndex((s) => s.id === id);
|
|
394
|
+
if (idx === -1) throw new Error(`workflow step not found: ${id}`);
|
|
395
|
+
return idx;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function pushOverride(workflow, entry) {
|
|
399
|
+
workflow.overrides = workflow.overrides || [];
|
|
400
|
+
workflow.overrides.push({ ts: new Date().toISOString(), ...entry });
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function audit(workflow, event, extra) {
|
|
404
|
+
workflow.audit = workflow.audit || [];
|
|
405
|
+
workflow.audit.push({ ts: new Date().toISOString(), event, ...(extra || {}) });
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function asList(value) {
|
|
409
|
+
if (!value) return [];
|
|
410
|
+
return Array.isArray(value) ? value : [value];
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function clone(value) {
|
|
414
|
+
return JSON.parse(JSON.stringify(value));
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
module.exports = {
|
|
418
|
+
PROTECTED_GATES,
|
|
419
|
+
BUILTIN_RECIPES,
|
|
420
|
+
recommendRecipe,
|
|
421
|
+
expandRecipe,
|
|
422
|
+
validateSteps,
|
|
423
|
+
createDraft,
|
|
424
|
+
addStep,
|
|
425
|
+
disableStep,
|
|
426
|
+
moveStep,
|
|
427
|
+
setVerifyReports,
|
|
428
|
+
confirmWorkflow,
|
|
429
|
+
assertStepAllowed,
|
|
430
|
+
completeStep,
|
|
431
|
+
diffWorkflow,
|
|
432
|
+
installedSkillExists,
|
|
433
|
+
};
|