@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,198 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const log = require('../../utils/log.js');
|
|
4
|
+
const state = require('../../core/state.js');
|
|
5
|
+
const checkpoint = require('../../core/checkpoint.js');
|
|
6
|
+
const wt = require('../../core/worktree.js');
|
|
7
|
+
const planTasks = require('../../core/plan-tasks.js');
|
|
8
|
+
const helpers = require('./_helpers.js');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* devflow apply [--task=N] [--note="..."] [--no-worktree] [--start|--done|--skip|--fail]
|
|
12
|
+
*
|
|
13
|
+
* Behavior:
|
|
14
|
+
* - Marks state.phases.apply.status = in_progress
|
|
15
|
+
* - state.iterations.apply ++ (counts how many "apply" calls in this change's life)
|
|
16
|
+
* - With --task=N:
|
|
17
|
+
* - Ensures a worktree exists (.devflow-worktrees/<slug>/task-N) — auto-create unless --no-worktree
|
|
18
|
+
* - Records/updates a task entry in state.phases.apply.tasks[]
|
|
19
|
+
* - Status transitions: --start | --done | --skip | --fail (default: in_progress)
|
|
20
|
+
* - --note appended to that task's notes[]
|
|
21
|
+
* - Without --task: just bumps iteration & writes a free-form note (legacy behavior)
|
|
22
|
+
*
|
|
23
|
+
* devflow apply does not write code itself — it tracks discipline + state.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
async function run({ flags = {}, positional = [], cwd }) {
|
|
27
|
+
const root = cwd || process.cwd();
|
|
28
|
+
const slug = await helpers.resolveSlug(root, flags, positional);
|
|
29
|
+
if (!slug) { process.exitCode = 1; return; }
|
|
30
|
+
const st = await helpers.loadStateOrFail(root, slug);
|
|
31
|
+
if (!st) { process.exitCode = 1; return; }
|
|
32
|
+
if (helpers.blockWorkflowStep(root, st, slug, 'apply', 'apply')) return;
|
|
33
|
+
|
|
34
|
+
const pendingPlanReview = checkpoint.latestPendingByType(st, 'plan_review');
|
|
35
|
+
if (pendingPlanReview && !flags.force) {
|
|
36
|
+
log.error('apply blocked: pending plan_review checkpoint.');
|
|
37
|
+
log.raw('');
|
|
38
|
+
log.raw(checkpoint.renderNextStepCard(pendingPlanReview));
|
|
39
|
+
log.raw('');
|
|
40
|
+
log.dim('override (audited): devflow apply --force --reason="..."');
|
|
41
|
+
process.exitCode = 1;
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (pendingPlanReview && flags.force) {
|
|
45
|
+
if (!flags.reason) {
|
|
46
|
+
log.error('--force requires --reason="..." when bypassing plan_review checkpoint');
|
|
47
|
+
process.exitCode = 2;
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
state.logEvent(st, 'apply.force_bypass_plan_review', { checkpointId: pendingPlanReview.id, reason: flags.reason });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const pendingReviewFix = checkpoint.latestPendingByType(st, 'review_fix');
|
|
54
|
+
if (pendingReviewFix) {
|
|
55
|
+
checkpoint.resolveCheckpoint(st, pendingReviewFix.id, 'fix');
|
|
56
|
+
state.logEvent(st, 'checkpoint.resolve', { id: pendingReviewFix.id, decision: 'fix' });
|
|
57
|
+
state.logEvent(st, 'apply.accept_review_fix', { checkpointId: pendingReviewFix.id });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
state.setPhase(st, 'plan', 'completed');
|
|
61
|
+
state.setPhase(st, 'apply', 'in_progress');
|
|
62
|
+
st.iterations = st.iterations || {}; st.iterations.apply = (st.iterations.apply || 0) + 1;
|
|
63
|
+
|
|
64
|
+
const taskRaw = flags.task;
|
|
65
|
+
const taskId = taskRaw !== undefined ? wt.normaliseTaskId(taskRaw) : null;
|
|
66
|
+
const note = flags.note;
|
|
67
|
+
const transition = pickTransition(flags);
|
|
68
|
+
const noWorktree = flags['no-worktree'] === true || flags.noWorktree === true || flags.worktree === false;
|
|
69
|
+
|
|
70
|
+
state.logEvent(st, 'apply', { task: taskId, transition, note });
|
|
71
|
+
|
|
72
|
+
let summary;
|
|
73
|
+
if (taskId) {
|
|
74
|
+
const apply = wt.ensureTaskBucket(st);
|
|
75
|
+
let task = apply.tasks.find((t) => t.id === taskId);
|
|
76
|
+
|
|
77
|
+
// Auto-create worktree if missing
|
|
78
|
+
if (!task && !noWorktree) {
|
|
79
|
+
try {
|
|
80
|
+
await state.write(root, slug, st);
|
|
81
|
+
const r = await wt.create(root, slug, { task: taskRaw, title: flags.title });
|
|
82
|
+
const re = await state.read(root, slug);
|
|
83
|
+
Object.assign(st, re);
|
|
84
|
+
task = wt.findTask(st, taskId);
|
|
85
|
+
if (r.created) log.ok(`auto-created worktree: ${path.relative(root, r.path)} branch=${r.branch}`);
|
|
86
|
+
} catch (e) {
|
|
87
|
+
log.warn(`worktree auto-create failed: ${e.message}`);
|
|
88
|
+
log.dim('continue tracking task without worktree (use --no-worktree to silence)');
|
|
89
|
+
task = ensureTaskRecord(st, taskId, { title: flags.title });
|
|
90
|
+
}
|
|
91
|
+
} else if (!task) {
|
|
92
|
+
task = ensureTaskRecord(st, taskId, { title: flags.title });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
applyTransition(task, transition);
|
|
96
|
+
if (flags.title) task.title = flags.title;
|
|
97
|
+
if (note) {
|
|
98
|
+
task.notes = task.notes || [];
|
|
99
|
+
task.notes.push({ ts: new Date().toISOString(), text: note });
|
|
100
|
+
}
|
|
101
|
+
task.updated_at = new Date().toISOString();
|
|
102
|
+
summary = `task=${task.id} status=${task.status} iterations=${st.iterations.apply}`;
|
|
103
|
+
if (task.worktree) summary += `\n worktree: ${path.relative(root, task.worktree)}`;
|
|
104
|
+
if (task.branch) summary += `\n branch: ${task.branch}`;
|
|
105
|
+
} else {
|
|
106
|
+
if (note) {
|
|
107
|
+
st.phases.apply.notes = st.phases.apply.notes || [];
|
|
108
|
+
st.phases.apply.notes.push({ ts: new Date().toISOString(), text: note });
|
|
109
|
+
}
|
|
110
|
+
summary = `iterations=${st.iterations.apply}`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
reconcileApplyStatus(root, slug, st);
|
|
114
|
+
if (st.phases.apply && st.phases.apply.status === 'completed') {
|
|
115
|
+
helpers.completeWorkflowStep(st, 'apply');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
await state.write(root, slug, st);
|
|
119
|
+
log.ok(`apply: ${summary}`);
|
|
120
|
+
log.dim(`next: keep coding (TDD!) or run "devflow review --slug=${slug} --round" when ready.`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function pickTransition(flags) {
|
|
124
|
+
if (flags.start === true) return 'start';
|
|
125
|
+
if (flags.done === true) return 'done';
|
|
126
|
+
if (flags.skip === true) return 'skip';
|
|
127
|
+
if (flags.fail === true) return 'fail';
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function applyTransition(task, t) {
|
|
132
|
+
const now = new Date().toISOString();
|
|
133
|
+
switch (t) {
|
|
134
|
+
case 'start':
|
|
135
|
+
task.status = 'in_progress';
|
|
136
|
+
if (!task.started_at) task.started_at = now;
|
|
137
|
+
break;
|
|
138
|
+
case 'done':
|
|
139
|
+
task.status = 'done';
|
|
140
|
+
task.ended_at = now;
|
|
141
|
+
break;
|
|
142
|
+
case 'skip':
|
|
143
|
+
task.status = 'skipped';
|
|
144
|
+
task.ended_at = now;
|
|
145
|
+
break;
|
|
146
|
+
case 'fail':
|
|
147
|
+
task.status = 'failed';
|
|
148
|
+
task.ended_at = now;
|
|
149
|
+
break;
|
|
150
|
+
default:
|
|
151
|
+
if (!task.status || task.status === 'pending') {
|
|
152
|
+
task.status = 'in_progress';
|
|
153
|
+
if (!task.started_at) task.started_at = now;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function ensureTaskRecord(st, taskId, fields) {
|
|
159
|
+
const apply = wt.ensureTaskBucket(st);
|
|
160
|
+
let task = apply.tasks.find((t) => t.id === taskId);
|
|
161
|
+
if (!task) {
|
|
162
|
+
task = {
|
|
163
|
+
id: taskId,
|
|
164
|
+
title: (fields && fields.title) || taskId,
|
|
165
|
+
status: 'pending',
|
|
166
|
+
notes: [],
|
|
167
|
+
started_at: new Date().toISOString(),
|
|
168
|
+
};
|
|
169
|
+
apply.tasks.push(task);
|
|
170
|
+
}
|
|
171
|
+
return task;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function reconcileApplyStatus(root, slug, st) {
|
|
175
|
+
const apply = wt.ensureTaskBucket(st);
|
|
176
|
+
const planIds = planTasks.readPlanTaskIds(root, slug);
|
|
177
|
+
const doneStates = new Set(['done', 'skipped']);
|
|
178
|
+
const statusById = new Map(apply.tasks.map((t) => [t.id, t.status]));
|
|
179
|
+
|
|
180
|
+
if (planIds.length > 0) {
|
|
181
|
+
const completed = planIds.filter((id) => doneStates.has(statusById.get(id)));
|
|
182
|
+
const remaining = planIds.filter((id) => !doneStates.has(statusById.get(id)));
|
|
183
|
+
apply.totalTasks = planIds.length;
|
|
184
|
+
apply.completedTasks = completed;
|
|
185
|
+
apply.currentTask = remaining[0] || null;
|
|
186
|
+
apply.status = remaining.length === 0 ? 'completed' : 'in_progress';
|
|
187
|
+
return apply;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
apply.completedTasks = apply.tasks.filter((t) => doneStates.has(t.status)).map((t) => t.id);
|
|
191
|
+
apply.currentTask = (apply.tasks.find((t) => !doneStates.has(t.status)) || {}).id || null;
|
|
192
|
+
if (apply.tasks.length > 0 && apply.tasks.every((t) => doneStates.has(t.status))) {
|
|
193
|
+
apply.status = 'completed';
|
|
194
|
+
}
|
|
195
|
+
return apply;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
module.exports = { run, reconcileApplyStatus };
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const fs = require('fs/promises');
|
|
3
|
+
const fsSync = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const readline = require('readline');
|
|
6
|
+
const log = require('../../utils/log.js');
|
|
7
|
+
const paths = require('../../core/paths.js');
|
|
8
|
+
const state = require('../../core/state.js');
|
|
9
|
+
const change = require('../../core/change.js');
|
|
10
|
+
const delta = require('../../core/delta.js');
|
|
11
|
+
const helpers = require('./_helpers.js');
|
|
12
|
+
const depositMod = require('../../knowledge/deposit.js');
|
|
13
|
+
const syncMod = require('../../knowledge/sync.js');
|
|
14
|
+
const loader = require('../../providers/loader.js');
|
|
15
|
+
|
|
16
|
+
/** Prompt user to choose external KB push mode. Returns 'mr' | 'push' | 'skip'. */
|
|
17
|
+
async function askKbPushMode(slug) {
|
|
18
|
+
// Non-interactive environments (CI, piped stdin) → skip
|
|
19
|
+
if (!process.stdin.isTTY || process.env.CI) return 'skip';
|
|
20
|
+
return new Promise((resolve) => {
|
|
21
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
22
|
+
log.raw('');
|
|
23
|
+
log.raw('┌─────────────────────────────────────────────────────┐');
|
|
24
|
+
log.raw(`│ 知识已沉淀到本地 devflow/knowledge/ │`);
|
|
25
|
+
log.raw(`│ 检测到外部知识库 (kb-git),如何同步? │`);
|
|
26
|
+
log.raw('│ │');
|
|
27
|
+
log.raw('│ [1] 创建 MR / PR(需审批,推荐) │');
|
|
28
|
+
log.raw('│ [2] 直接推送到主分支(无需审批,快速) │');
|
|
29
|
+
log.raw('│ [3] 跳过,稍后手动处理 │');
|
|
30
|
+
log.raw('└─────────────────────────────────────────────────────┘');
|
|
31
|
+
rl.question(' 请选择 [1/2/3,默认 1]:', (ans) => {
|
|
32
|
+
rl.close();
|
|
33
|
+
const choice = (ans || '1').trim();
|
|
34
|
+
if (choice === '2') resolve('push');
|
|
35
|
+
else if (choice === '3') resolve('skip');
|
|
36
|
+
else resolve('mr');
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function run({ flags = {}, positional = [], cwd }) {
|
|
42
|
+
const root = cwd || process.cwd();
|
|
43
|
+
const slug = await helpers.resolveSlug(root, flags, positional);
|
|
44
|
+
if (!slug) { process.exitCode = 1; return; }
|
|
45
|
+
const st = await helpers.loadStateOrFail(root, slug);
|
|
46
|
+
if (!st) { process.exitCode = 1; return; }
|
|
47
|
+
|
|
48
|
+
if (st.mode === 'micro' || st.level === 'L0') {
|
|
49
|
+
log.error('archive skipped: L0 micro changes do not enter archive.');
|
|
50
|
+
log.dim('If there is knowledge to keep, run: devflow knowledge deposit --slug=' + slug);
|
|
51
|
+
process.exitCode = 1;
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// 1. Merge spec deltas
|
|
56
|
+
log.info('merging spec deltas...');
|
|
57
|
+
const { merged, conflicts } = await delta.mergeChangeDeltas(root, slug, { dryRun: flags['dry-run'] });
|
|
58
|
+
if (conflicts.length) {
|
|
59
|
+
log.error('spec delta merge has conflicts:');
|
|
60
|
+
for (const c of conflicts) {
|
|
61
|
+
log.error(` ${c.file}: ${c.conflicts.map((x) => `${x.kind}/${x.heading} (${x.reason})`).join(', ')}`);
|
|
62
|
+
}
|
|
63
|
+
log.dim('resolve in delta/*.md or pass --force to apply anyway.');
|
|
64
|
+
if (!flags.force) { process.exitCode = 1; return; }
|
|
65
|
+
}
|
|
66
|
+
if (merged.length === 0) {
|
|
67
|
+
log.warn('no spec delta files found — devflow/specs/ NOT updated.');
|
|
68
|
+
log.warn(' (hint: agent should have created delta/*.md during devflow design)');
|
|
69
|
+
}
|
|
70
|
+
for (const m of merged) log.ok(`spec: ${path.relative(root, m.target)}`);
|
|
71
|
+
|
|
72
|
+
if (flags['dry-run']) {
|
|
73
|
+
log.warn('dry-run: archive folder NOT moved, knowledge deposit skipped.');
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 2. Auto knowledge deposit (before moving the folder, while change dir still exists)
|
|
78
|
+
const skipDeposit = !!(flags['skip-deposit'] || flags.skipDeposit);
|
|
79
|
+
let depositedItems = [];
|
|
80
|
+
if (!skipDeposit) {
|
|
81
|
+
log.info('knowledge deposit: scanning knowledge/ + design/reports for experience…');
|
|
82
|
+
try {
|
|
83
|
+
const plan = await depositMod.deposit(root, { slug, dryRun: true });
|
|
84
|
+
const actionable = plan.filter((p) => p.action !== 'noop');
|
|
85
|
+
if (actionable.length > 0) {
|
|
86
|
+
await depositMod.executeItems(root, actionable, slug);
|
|
87
|
+
depositedItems = actionable;
|
|
88
|
+
const created = actionable.filter((p) => p.action === 'create').length;
|
|
89
|
+
const updated = actionable.filter((p) => p.action === 'update').length;
|
|
90
|
+
for (const p of actionable) {
|
|
91
|
+
log.dim(` ${p.action === 'create' ? 'A' : 'M'} ${p.domain}/${p.category}/${p.filename}`);
|
|
92
|
+
}
|
|
93
|
+
log.ok(`knowledge: create=${created} update=${updated} → workspace knowledge/`);
|
|
94
|
+
|
|
95
|
+
// 2b. Sync to external KB provider (git or weknora)
|
|
96
|
+
const depositMode = flags['deposit-mode'] || flags.depositMode;
|
|
97
|
+
let kbProvider = null;
|
|
98
|
+
let kbDriver = null;
|
|
99
|
+
try {
|
|
100
|
+
const p = await loader.load(root, { type: 'kb' });
|
|
101
|
+
if (p) { kbProvider = p; kbDriver = p.driver; }
|
|
102
|
+
} catch { /* no kb provider configured */ }
|
|
103
|
+
|
|
104
|
+
if (kbProvider && kbDriver === 'git') {
|
|
105
|
+
// ── Git-backed KB: ask MR vs direct push ──────────────────────────
|
|
106
|
+
let mode = depositMode;
|
|
107
|
+
if (!mode) mode = await askKbPushMode(slug);
|
|
108
|
+
|
|
109
|
+
if (mode === 'mr') {
|
|
110
|
+
log.info('知识库:创建 MR / PR 中…');
|
|
111
|
+
try {
|
|
112
|
+
const result = await kbProvider.depositAndCreateMr(root, { items: actionable, slug });
|
|
113
|
+
if (result.url) log.ok(`知识库 MR 已创建:${result.url}`);
|
|
114
|
+
else if (result.status === 'pushed_no_mr') log.warn(`已推送分支 ${result.branch},MR 创建失败:${result.error}`);
|
|
115
|
+
else if (result.status === 'no_changes') log.dim('知识库:无变更(外部仓库已是最新)');
|
|
116
|
+
} catch (e) {
|
|
117
|
+
log.warn(`知识库 MR 创建失败:${e.message}`);
|
|
118
|
+
}
|
|
119
|
+
} else if (mode === 'push') {
|
|
120
|
+
log.info('知识库:直接推送中…');
|
|
121
|
+
try {
|
|
122
|
+
const result = await kbProvider.depositAndPush(root, { items: actionable, slug });
|
|
123
|
+
if (result.status === 'pushed') log.ok(`知识库:已直接推送到 ${result.branch}(${result.commit_hash?.slice(0, 7)})`);
|
|
124
|
+
else if (result.status === 'no_changes') log.dim('知识库:无变更');
|
|
125
|
+
} catch (e) {
|
|
126
|
+
log.warn(`知识库推送失败:${e.message}`);
|
|
127
|
+
}
|
|
128
|
+
} else {
|
|
129
|
+
log.dim('知识库:已跳过外部同步。稍后可运行 `devflow knowledge deposit --mr` 或 `--push`');
|
|
130
|
+
}
|
|
131
|
+
} else if (kbProvider) {
|
|
132
|
+
// ── WeKnora / other API-based KB: auto sync immediately ───────────
|
|
133
|
+
log.info('知识库:同步到远端 KB 中…');
|
|
134
|
+
try {
|
|
135
|
+
const { summary } = await syncMod.sync(root, { dryRun: false });
|
|
136
|
+
log.ok(`知识库同步完成:create=${summary.create} update=${summary.update} delete=${summary.delete} noop=${summary.noop}`);
|
|
137
|
+
} catch (e) {
|
|
138
|
+
log.warn(`知识库同步失败:${e.message}`);
|
|
139
|
+
log.dim(' 稍后可手动运行 `devflow knowledge sync` 重试');
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
} else {
|
|
143
|
+
log.dim('knowledge: nothing new to deposit (no knowledge/ files, design sections, or incident reports?)');
|
|
144
|
+
}
|
|
145
|
+
} catch (err) {
|
|
146
|
+
log.warn(`knowledge deposit skipped: ${err.message}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// 3. Drop legacy duplicate learnings.md before archiving.
|
|
151
|
+
// Knowledge entries live under knowledge/ now; keeping both creates drift.
|
|
152
|
+
const src = change.resolveChangeDir(root, slug);
|
|
153
|
+
const legacyLearnings = path.join(src, 'learnings.md');
|
|
154
|
+
if (fsSync.existsSync(legacyLearnings)) {
|
|
155
|
+
await fs.rm(legacyLearnings, { force: true });
|
|
156
|
+
log.dim('removed legacy learnings.md (knowledge/ is the source of truth)');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// 4. Move change folder to archive/
|
|
160
|
+
const dateStr = new Date().toISOString().slice(0, 10);
|
|
161
|
+
const targetName = `${dateStr}-${slug}`;
|
|
162
|
+
const dst = path.join(paths.archiveDir(root), targetName);
|
|
163
|
+
await fs.mkdir(paths.archiveDir(root), { recursive: true });
|
|
164
|
+
await fs.rename(src, dst);
|
|
165
|
+
log.ok(`archive: devflow/archive/${targetName}/`);
|
|
166
|
+
|
|
167
|
+
// 5. Final state log (write into archived folder)
|
|
168
|
+
const stateFile = path.join(dst, 'state.json');
|
|
169
|
+
if (fsSync.existsSync(stateFile)) {
|
|
170
|
+
const archived = JSON.parse(await fs.readFile(stateFile, 'utf8'));
|
|
171
|
+
state.setPhase(archived, 'archive', 'completed');
|
|
172
|
+
state.logEvent(archived, 'archived');
|
|
173
|
+
archived.updatedAt = new Date().toISOString();
|
|
174
|
+
await fs.writeFile(stateFile, JSON.stringify(archived, null, 2) + '\n', 'utf8');
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
log.ok(`workflow complete for ${slug}`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
module.exports = { run };
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const fsSync = require('fs');
|
|
3
|
+
const log = require('../../utils/log.js');
|
|
4
|
+
const paths = require('../../core/paths.js');
|
|
5
|
+
const change = require('../../core/change.js');
|
|
6
|
+
const stateMod = require('../../core/state.js');
|
|
7
|
+
const checkpointMod = require('../../core/checkpoint.js');
|
|
8
|
+
|
|
9
|
+
async function run({ positional = [], flags = {}, cwd }) {
|
|
10
|
+
const root = cwd || process.cwd();
|
|
11
|
+
if (!fsSync.existsSync(paths.devflowDir(root))) {
|
|
12
|
+
log.warn('not a devflow project. run "devflow init" first.');
|
|
13
|
+
process.exitCode = 1;
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const action = positional[0] || 'show';
|
|
18
|
+
const slug = flags.slug || (await change.getCurrent(root));
|
|
19
|
+
if (!slug) {
|
|
20
|
+
log.warn('no change found. run "devflow ingest <input>" or "devflow new <slug>".');
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const st = await stateMod.read(root, slug);
|
|
25
|
+
|
|
26
|
+
if (action === 'add' || action === 'create') {
|
|
27
|
+
const cp = checkpointMod.addCheckpoint(st, {
|
|
28
|
+
type: flags.type,
|
|
29
|
+
phase: flags.phase || st.currentPhase,
|
|
30
|
+
summary: flags.summary,
|
|
31
|
+
question: flags.question,
|
|
32
|
+
options: checkpointMod.parseOptions(flags.options),
|
|
33
|
+
defaultOption: flags.default || flags.defaultOption,
|
|
34
|
+
nextAction: flags.nextAction || flags.command,
|
|
35
|
+
risks: splitList(flags.risks),
|
|
36
|
+
evidence: splitList(flags.evidence),
|
|
37
|
+
});
|
|
38
|
+
stateMod.logEvent(st, 'checkpoint.add', { id: cp.id, type: cp.type, phase: cp.phase });
|
|
39
|
+
await stateMod.write(root, slug, st);
|
|
40
|
+
log.ok(`checkpoint added: ${cp.id}`);
|
|
41
|
+
log.raw('');
|
|
42
|
+
log.raw(checkpointMod.renderNextStepCard(cp));
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (action === 'resolve') {
|
|
47
|
+
const id = flags.id || positional[1];
|
|
48
|
+
const cp = checkpointMod.resolveCheckpoint(st, id, flags.decision || flags.option, flags.note);
|
|
49
|
+
stateMod.logEvent(st, 'checkpoint.resolve', { id: cp.id, decision: cp.decision });
|
|
50
|
+
await stateMod.write(root, slug, st);
|
|
51
|
+
log.ok(`checkpoint resolved: ${cp.id}`);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (action === 'cancel') {
|
|
56
|
+
const id = flags.id || positional[1];
|
|
57
|
+
const cp = checkpointMod.cancelCheckpoint(st, id, flags.reason);
|
|
58
|
+
stateMod.logEvent(st, 'checkpoint.cancel', { id: cp.id, reason: cp.reason });
|
|
59
|
+
await stateMod.write(root, slug, st);
|
|
60
|
+
log.ok(`checkpoint cancelled: ${cp.id}`);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (action !== 'show') {
|
|
65
|
+
log.error(`unknown checkpoint action: ${action}`);
|
|
66
|
+
process.exitCode = 2;
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const pending = checkpointMod.latestPending(st);
|
|
71
|
+
if (!pending) {
|
|
72
|
+
if (flags.json === true) {
|
|
73
|
+
log.raw(JSON.stringify({ pending: null, confirmationCard: null, nextAction: null }, null, 2));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
log.ok('no pending checkpoint');
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (flags.json === true) {
|
|
80
|
+
log.raw(JSON.stringify(buildShowResponse(slug, pending), null, 2));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
log.raw(checkpointMod.renderNextStepCard(pending));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function buildShowResponse(slug, pending) {
|
|
87
|
+
return {
|
|
88
|
+
type: 'checkpoint_confirmation_surface',
|
|
89
|
+
slug,
|
|
90
|
+
pending,
|
|
91
|
+
confirmationCard: checkpointMod.buildConfirmationCard(pending, confirmationCardOptions(pending)),
|
|
92
|
+
nextAction: pending.nextAction || (pending.options && pending.options[0] && pending.options[0].command) || null,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function confirmationCardOptions(checkpoint) {
|
|
97
|
+
if (checkpoint.type === 'workflow_policy') {
|
|
98
|
+
return {
|
|
99
|
+
type: 'workflow_policy_confirm',
|
|
100
|
+
title: '确认 workflow 风险',
|
|
101
|
+
primaryActionId: 'accept-risk',
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
return {};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function splitList(value) {
|
|
108
|
+
if (!value) return [];
|
|
109
|
+
if (Array.isArray(value)) return value;
|
|
110
|
+
return String(value).split(';').map((s) => s.trim()).filter(Boolean);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
module.exports = { run };
|