@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.
Files changed (198) hide show
  1. package/CHANGELOG.md +232 -0
  2. package/LICENSE +21 -0
  3. package/README.md +539 -0
  4. package/bin/devflow.js +9 -0
  5. package/docs/RFC-001-devflow-kit.md +617 -0
  6. package/docs/RFC-002-workflow-kernel.md +134 -0
  7. package/docs/enterprise-integration-supplement.md +274 -0
  8. package/docs/internal-gitlab-setup.md +426 -0
  9. package/docs/marketplace-skills.md +231 -0
  10. package/docs/migration-from-arb.md +232 -0
  11. package/docs/tooling-overview.md +774 -0
  12. package/docs/workflow-orchestration.md +695 -0
  13. package/docs/workflow-ui-prototype.html +271 -0
  14. package/package.json +52 -0
  15. package/schemas/config.schema.json +51 -0
  16. package/schemas/delta.schema.json +22 -0
  17. package/schemas/state.schema.json +130 -0
  18. package/schemas/status-surface.schema.json +197 -0
  19. package/schemas/workflow-confirmation-surface.schema.json +70 -0
  20. package/schemas/workflow-picker.schema.json +94 -0
  21. package/scripts/postinstall.js +101 -0
  22. package/scripts/render-workflow-ui-prototype.js +271 -0
  23. package/skills/apply/SKILL.md +313 -0
  24. package/skills/apply/references/discipline-checklist.md +145 -0
  25. package/skills/apply/references/subagent-implementer-prompt.md +113 -0
  26. package/skills/apply/references/subagent-orchestration.md +150 -0
  27. package/skills/apply/references/subagent-reviewer-prompt.md +180 -0
  28. package/skills/apply/references/tdd-loop.md +287 -0
  29. package/skills/apply/references/when-plan-is-wrong.md +279 -0
  30. package/skills/apply/references/worktree-swarm.md +292 -0
  31. package/skills/archive/SKILL.md +229 -0
  32. package/skills/archive/references/conflict-resolution.md +336 -0
  33. package/skills/archive/references/knowledge-deposit.md +381 -0
  34. package/skills/archive/references/spec-merge.md +365 -0
  35. package/skills/brainstorm/SKILL.md +123 -0
  36. package/skills/brainstorm/references/proposal-template.md +244 -0
  37. package/skills/brainstorm/references/question-catalog.md +168 -0
  38. package/skills/brainstorm/references/session-template.md +184 -0
  39. package/skills/ci-fix/SKILL.md +63 -0
  40. package/skills/ci-fix/references/loop.md +25 -0
  41. package/skills/code-review/SKILL.md +279 -0
  42. package/skills/code-review/references/escalation-playbook.md +192 -0
  43. package/skills/code-review/references/language-cheatsheets/go.md +175 -0
  44. package/skills/code-review/references/language-cheatsheets/java-spring-mybatis.md +246 -0
  45. package/skills/code-review/references/language-cheatsheets/python.md +170 -0
  46. package/skills/code-review/references/language-cheatsheets/vue.md +199 -0
  47. package/skills/code-review/references/output-template.md +275 -0
  48. package/skills/code-review/references/review-checklist.md +251 -0
  49. package/skills/complexity-grading/SKILL.md +259 -0
  50. package/skills/deliver/SKILL.md +271 -0
  51. package/skills/deliver/references/delivery-modes.md +299 -0
  52. package/skills/deliver/references/notify.md +359 -0
  53. package/skills/deliver/references/pr-description.md +319 -0
  54. package/skills/dependency-upgrade/SKILL.md +57 -0
  55. package/skills/dependency-upgrade/references/risk-matrix.md +38 -0
  56. package/skills/df-orchestrator/SKILL.md +407 -0
  57. package/skills/df-orchestrator/references/complexity-grading.md +177 -0
  58. package/skills/df-orchestrator/references/escalation-matrix.md +191 -0
  59. package/skills/df-orchestrator/references/routing-rules.md +290 -0
  60. package/skills/df-orchestrator/references/workflow-state-machine.md +208 -0
  61. package/skills/frontend-quality/SKILL.md +61 -0
  62. package/skills/frontend-quality/references/checklist.md +35 -0
  63. package/skills/handoff-resume/SKILL.md +59 -0
  64. package/skills/handoff-resume/references/handoff-template.md +54 -0
  65. package/skills/plan/SKILL.md +166 -0
  66. package/skills/plan/references/task-breakdown.md +207 -0
  67. package/skills/plan/references/task-sequencing.md +143 -0
  68. package/skills/plan/references/task-template.md +248 -0
  69. package/skills/requirement-analysis/SKILL.md +499 -0
  70. package/skills/requirement-analysis/references/acceptance-criteria.md +183 -0
  71. package/skills/requirement-analysis/references/code-recon.md +151 -0
  72. package/skills/requirement-analysis/references/edge-case-catalog.md +164 -0
  73. package/skills/requirement-analysis/references/requirement-template.md +339 -0
  74. package/skills/requirement-analysis/references/scope-negotiation.md +162 -0
  75. package/skills/security-hardening/SKILL.md +60 -0
  76. package/skills/security-hardening/references/checklist.md +42 -0
  77. package/skills/tech-spec/SKILL.md +388 -0
  78. package/skills/tech-spec/references/api-contract-design.md +172 -0
  79. package/skills/tech-spec/references/decision-records.md +110 -0
  80. package/skills/tech-spec/references/design-template.md +301 -0
  81. package/skills/tech-spec/references/rollout-and-rollback.md +203 -0
  82. package/skills/tech-spec/references/spec-delta-conventions.md +250 -0
  83. package/skills/tech-spec/references/transaction-patterns.md +212 -0
  84. package/skills/test-spec/SKILL.md +219 -0
  85. package/skills/test-spec/references/coverage-strategy.md +218 -0
  86. package/skills/test-spec/references/edge-case-to-test.md +143 -0
  87. package/skills/test-spec/references/test-case-template.md +276 -0
  88. package/skills/verify/SKILL.md +232 -0
  89. package/skills/verify/references/nfr-verification.md +292 -0
  90. package/skills/verify/references/report-templates.md +510 -0
  91. package/skills/verify/references/self-test-guide.md +240 -0
  92. package/skills/verify/references/verify-rollback-map.md +247 -0
  93. package/src/cli/commands/_helpers.js +108 -0
  94. package/src/cli/commands/_submit.js +718 -0
  95. package/src/cli/commands/apply.js +198 -0
  96. package/src/cli/commands/archive.js +180 -0
  97. package/src/cli/commands/checkpoint.js +113 -0
  98. package/src/cli/commands/deliver.js +377 -0
  99. package/src/cli/commands/deploy.js +504 -0
  100. package/src/cli/commands/design.js +158 -0
  101. package/src/cli/commands/disable.js +21 -0
  102. package/src/cli/commands/doctor.js +178 -0
  103. package/src/cli/commands/enable.js +21 -0
  104. package/src/cli/commands/flow.js +645 -0
  105. package/src/cli/commands/help.js +93 -0
  106. package/src/cli/commands/ingest.js +602 -0
  107. package/src/cli/commands/init.js +341 -0
  108. package/src/cli/commands/knowledge.js +523 -0
  109. package/src/cli/commands/logs.js +43 -0
  110. package/src/cli/commands/new.js +202 -0
  111. package/src/cli/commands/plan.js +49 -0
  112. package/src/cli/commands/propose.js +27 -0
  113. package/src/cli/commands/provider.js +698 -0
  114. package/src/cli/commands/report.js +143 -0
  115. package/src/cli/commands/requirement.js +227 -0
  116. package/src/cli/commands/review.js +301 -0
  117. package/src/cli/commands/skills.js +457 -0
  118. package/src/cli/commands/status.js +925 -0
  119. package/src/cli/commands/switch.js +27 -0
  120. package/src/cli/commands/sync.js +47 -0
  121. package/src/cli/commands/test.js +366 -0
  122. package/src/cli/commands/uninstall.js +32 -0
  123. package/src/cli/commands/update.js +74 -0
  124. package/src/cli/commands/verify.js +354 -0
  125. package/src/cli/commands/worktree.js +78 -0
  126. package/src/cli/index.js +72 -0
  127. package/src/cli/parse-args.js +102 -0
  128. package/src/core/autodetect.js +271 -0
  129. package/src/core/change.js +208 -0
  130. package/src/core/checkpoint.js +217 -0
  131. package/src/core/config.js +60 -0
  132. package/src/core/delta.js +290 -0
  133. package/src/core/markers.js +59 -0
  134. package/src/core/paths.js +173 -0
  135. package/src/core/plan-tasks.js +36 -0
  136. package/src/core/project-routing.js +285 -0
  137. package/src/core/projects.js +200 -0
  138. package/src/core/state.js +200 -0
  139. package/src/core/workflow-check.js +177 -0
  140. package/src/core/workflow-init.js +34 -0
  141. package/src/core/workflow-picker.js +154 -0
  142. package/src/core/workflow-policy.js +119 -0
  143. package/src/core/workflow-suggest.js +181 -0
  144. package/src/core/workflow-verify.js +88 -0
  145. package/src/core/workflow.js +433 -0
  146. package/src/core/worktree.js +241 -0
  147. package/src/knowledge/categories.js +107 -0
  148. package/src/knowledge/classify.js +125 -0
  149. package/src/knowledge/deposit.js +414 -0
  150. package/src/knowledge/migrate.js +149 -0
  151. package/src/knowledge/mr.js +219 -0
  152. package/src/knowledge/query.js +131 -0
  153. package/src/knowledge/registry.js +151 -0
  154. package/src/knowledge/sync.js +179 -0
  155. package/src/providers/base.js +74 -0
  156. package/src/providers/drivers/api-yapi.js +78 -0
  157. package/src/providers/drivers/ci-jenkins.js +109 -0
  158. package/src/providers/drivers/intake-confluence.js +544 -0
  159. package/src/providers/drivers/kb-git.js +549 -0
  160. package/src/providers/drivers/kb-weknora.js +472 -0
  161. package/src/providers/drivers/notify-smtp.js +515 -0
  162. package/src/providers/drivers/observability-oss.js +43 -0
  163. package/src/providers/drivers/observability-sls.js +50 -0
  164. package/src/providers/lifecycle.js +135 -0
  165. package/src/providers/loader.js +132 -0
  166. package/src/providers/local.js +190 -0
  167. package/src/providers/userconfig.js +283 -0
  168. package/src/reports/aggregate.js +185 -0
  169. package/src/reports/coverage.js +163 -0
  170. package/src/reports/detect.js +143 -0
  171. package/src/reports/parse.js +236 -0
  172. package/src/templates/files/ci/github.yml +38 -0
  173. package/src/templates/files/ci/gitlab.yml +27 -0
  174. package/src/templates/files/design.md +63 -0
  175. package/src/templates/files/ide/devflow-workflow.md +58 -0
  176. package/src/templates/files/ide/project-overview-reference.md +1 -0
  177. package/src/templates/files/ide/project-overview.md +27 -0
  178. package/src/templates/files/knowledge-index.json +17 -0
  179. package/src/templates/files/knowledge.md +28 -0
  180. package/src/templates/files/meta.json +8 -0
  181. package/src/templates/files/plan.md +38 -0
  182. package/src/templates/files/proposal.md +33 -0
  183. package/src/templates/files/reports/contract-test.md +40 -0
  184. package/src/templates/files/reports/e2e-test.md +30 -0
  185. package/src/templates/files/reports/integration-test.md +36 -0
  186. package/src/templates/files/reports/joint-test.md +58 -0
  187. package/src/templates/files/reports/perf.md +24 -0
  188. package/src/templates/files/reports/regression.md +20 -0
  189. package/src/templates/files/reports/remote-test.md +55 -0
  190. package/src/templates/files/reports/self-test.md +43 -0
  191. package/src/templates/files/reports/smoke-test.md +22 -0
  192. package/src/templates/files/reports/unit-test.md +36 -0
  193. package/src/templates/files/requirement.md +51 -0
  194. package/src/templates/files/review.md +38 -0
  195. package/src/templates/files/tests.md +36 -0
  196. package/src/templates/files/verify.md +32 -0
  197. package/src/templates/index.js +21 -0
  198. package/src/utils/log.js +37 -0
@@ -0,0 +1,377 @@
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 state = require('../../core/state.js');
8
+ const change = require('../../core/change.js');
9
+ const checkpoint = require('../../core/checkpoint.js');
10
+ const providers = require('../../providers/loader.js');
11
+ const helpers = require('./_helpers.js');
12
+ const doctor = require('./doctor.js');
13
+ const config = require('../../core/config.js');
14
+ const autodetect = require('../../core/autodetect.js');
15
+ const submit = require('./_submit.js');
16
+ const aggregate = require('../../reports/aggregate.js');
17
+
18
+ async function run({ flags = {}, positional = [], cwd }) {
19
+ const root = cwd || process.cwd();
20
+ const slug = await helpers.resolveSlug(root, flags, positional);
21
+ if (!slug) { process.exitCode = 1; return; }
22
+ const st = await helpers.loadStateOrFail(root, slug);
23
+ if (!st) { process.exitCode = 1; return; }
24
+ if (helpers.blockWorkflowStep(root, st, slug, 'deliver', 'deliver')) return;
25
+
26
+ if (await blockPendingRiskCheckpoint(root, slug, st, flags)) return;
27
+ if (await blockOpenRiskSignals(root, slug, st, flags)) return;
28
+
29
+ // Preflight: verify phase must be completed
30
+ const verifyPhase = st.phases && st.phases.verify;
31
+ if (!verifyPhase || verifyPhase.status !== 'completed') {
32
+ log.error('verify phase not completed. run "devflow verify finalize" first.');
33
+ process.exitCode = 1;
34
+ return;
35
+ }
36
+
37
+ // Preflight: review phase must be completed (redundant, but explicit)
38
+ const reviewPhase = st.phases && st.phases.review;
39
+ if (!reviewPhase || reviewPhase.status !== 'completed') {
40
+ log.error(`deliver blocked: review.status=${(reviewPhase && reviewPhase.status) || 'pending'}. finish review first.`);
41
+ process.exitCode = 1;
42
+ return;
43
+ }
44
+
45
+ // Preflight: devflow doctor --scope change,git,cred (continue only if no errors, unless --skip-doctor with reason)
46
+ const skipDoctor = flags['skip-doctor'] === true || flags.skipDoctor === true;
47
+ if (!skipDoctor) {
48
+ const { findings, errors } = await doctor.audit(root, ['change', 'git', 'cred']);
49
+ if (errors > 0) {
50
+ log.error('deliver blocked: devflow doctor found errors:');
51
+ for (const f of findings) {
52
+ if (f.level === 'error') log.error(` [${f.scope}] ${f.msg}`);
53
+ else if (f.level === 'warn') log.warn(` [${f.scope}] ${f.msg}`);
54
+ }
55
+ log.dim('fix the errors, or override (audited): devflow deliver --skip-doctor --reason="..."');
56
+ process.exitCode = 1;
57
+ return;
58
+ }
59
+ } else {
60
+ const reason = flags.reason;
61
+ if (!reason || reason === true) {
62
+ log.error('--skip-doctor requires --reason="..." (audited)');
63
+ process.exitCode = 2;
64
+ return;
65
+ }
66
+ state.logEvent(st, 'deliver.skip_doctor', { reason });
67
+ log.warn(`doctor preflight skipped — reason: "${reason}"`);
68
+ }
69
+
70
+ const mode = flags.mode || positional[1] || 'pr';
71
+ const isMicro = st.mode === 'micro' || st.level === 'L0';
72
+ log.info(`deliver mode=${mode}`);
73
+
74
+ // Resolve detect / branches once — used by both PR and notify paths
75
+ const cfg = await config.read(root).catch(() => null);
76
+ const detect = (cfg && cfg.detect) || (await autodetect.detect(root).catch(() => null)) || {};
77
+ const base = flags.base
78
+ || (cfg && cfg.detect && cfg.detect.defaultBranch)
79
+ || autodetect.detectDefaultBranch(root)
80
+ || 'main';
81
+ const head = flags.head || submit.primaryProjectBranch(st) || submit.currentBranch(root) || '';
82
+
83
+ // ─── PR description body ────────────────────────────────────────────────
84
+ // For PR / MR: keep self-test embed + doc links (reviewers want diffs not full submit doc).
85
+ const cdir = change.resolveChangeDir(root, slug);
86
+ const selfTestPath = path.join(cdir, 'reports', 'self-test.md');
87
+ const aggregateSelfTest = aggregate.readAggregate(path.join(cdir, 'reports')).reports.find((r) => r.kind === 'self-test');
88
+ let prBody = `# ${st.title || slug}\n\n`;
89
+ prBody += `> change: \`${slug}\` level: \`${st.level}\` base ← head: \`${base}\` ← \`${head || '(current)'}\`\n\n`;
90
+ if (aggregateSelfTest || fsSync.existsSync(selfTestPath)) {
91
+ prBody += `## 提测自测\n\n`;
92
+ prBody += aggregateSelfTest ? aggregateSelfTest.body : await fs.readFile(selfTestPath, 'utf8');
93
+ prBody += `\n`;
94
+ }
95
+ prBody += `\n## 关联文档\n\n`;
96
+ for (const f of ['proposal.md', 'requirement.md', 'design.md', 'plan.md', 'review.md', 'verify.md']) {
97
+ if (fsSync.existsSync(path.join(cdir, f))) {
98
+ prBody += `- [\`${f}\`](devflow/changes/${slug}/${f})\n`;
99
+ }
100
+ }
101
+
102
+ if (mode === 'pr') {
103
+ let vcs;
104
+ try { vcs = await providers.load(root, { type: 'vcs' }); }
105
+ catch (e) { log.error(e.message); process.exitCode = 1; return; }
106
+ const result = await vcs.pr({
107
+ title: `[${slug}] ${st.title || slug}`,
108
+ body_md: prBody,
109
+ base, head,
110
+ draft: flags.draft === true,
111
+ });
112
+ if (result.url) log.ok(`PR: ${result.url}`);
113
+ } else if (mode === 'merge') {
114
+ log.warn('merge mode is a stub in v0.1; merge manually then run "devflow archive" to merge specs and deposit knowledge.');
115
+ } else if (mode === 'keep') {
116
+ log.info('keeping change open. run "devflow deliver --mode=pr" later.');
117
+ } else if (mode === 'discard') {
118
+ log.warn('discard mode is a stub. delete devflow/changes/<slug>/ manually.');
119
+ }
120
+
121
+ // ─── Notify (提测邮件) ─────────────────────────────────────────────────
122
+ // Mirrors arb deploy-submit Step 3-5:
123
+ // 3. render a 提测单.md as the email body (not just a self-test embed)
124
+ // 4. human confirmation (subject + recipients + attachments preview)
125
+ // 5. send via SMTP, attach only the consolidated reports/test-report.md by default
126
+ const notifyDisabled = flags.notify === false;
127
+ const wantsNotify = !notifyDisabled && (flags.notify === true
128
+ || flags.notify === 'test-report'
129
+ || flags['notify-dry-run'] === true
130
+ || isMicro);
131
+ if (wantsNotify) {
132
+ const notifyFlags = { ...flags };
133
+ if (isMicro && flags.notify === undefined && flags['notify-dry-run'] !== true) {
134
+ notifyFlags.notify = 'test-report';
135
+ notifyFlags['no-confirm'] = true;
136
+ notifyFlags.noConfirm = true;
137
+ state.logEvent(st, 'deliver.micro_test_report_notify', { defaulted: true });
138
+ }
139
+ await runNotify({ root, slug, st, base, head, detect, flags: notifyFlags });
140
+ }
141
+
142
+ state.setPhase(st, 'deliver', isMicro ? 'completed' : 'in_progress');
143
+ helpers.completeWorkflowStep(st, 'deliver');
144
+ state.logEvent(st, 'deliver', { mode });
145
+ await state.write(root, slug, st);
146
+ if (isMicro) {
147
+ log.dim('next: L0 micro flow ends after deliver; archive is skipped.');
148
+ } else {
149
+ log.dim('next: after MR is merged / change is live, run "devflow archive"');
150
+ log.dim(' archive will merge specs, deposit workspace knowledge, and sync or submit KB changes when configured.');
151
+ }
152
+ }
153
+
154
+ async function blockOpenRiskSignals(root, slug, st, flags) {
155
+ const open = state.openRiskSignals(st);
156
+ if (!open.length) return false;
157
+ if (!flags.force) {
158
+ log.error(`deliver blocked: open risk signals: ${open.map((r) => r.type).join(', ')}`);
159
+ log.dim('resolve them with verification evidence, or override (audited): devflow deliver --force --reason="..."');
160
+ process.exitCode = 1;
161
+ return true;
162
+ }
163
+ const reason = flags.reason;
164
+ if (!reason || reason === true) {
165
+ log.error('deliver --force requires --reason="..." when accepting open risk signals');
166
+ process.exitCode = 2;
167
+ return true;
168
+ }
169
+ for (const r of open) {
170
+ state.acceptRiskSignal(st, r.type, { reason, evidence: ['deliver force acceptance'] });
171
+ }
172
+ state.logEvent(st, 'deliver.force_accept_risk_signals', {
173
+ riskSignals: open.map((r) => r.type),
174
+ reason,
175
+ });
176
+ await state.write(root, slug, st);
177
+ log.warn(`deliver: force-accepted open risk signals — ${open.map((r) => r.type).join(', ')}`);
178
+ return false;
179
+ }
180
+
181
+ async function blockPendingRiskCheckpoint(root, slug, st, flags) {
182
+ const pending = checkpoint.latestPendingByTypes(st, [
183
+ 'review_fix',
184
+ 'review_blocked',
185
+ 'verification_evidence',
186
+ 'risk_acceptance',
187
+ ]);
188
+ if (!pending) return false;
189
+ if (!flags.force) {
190
+ log.error(`deliver blocked: pending ${pending.type} checkpoint.`);
191
+ log.raw('');
192
+ log.raw(checkpoint.renderNextStepCard(pending));
193
+ log.dim('override (audited): devflow deliver --force --reason="..."');
194
+ process.exitCode = 1;
195
+ return true;
196
+ }
197
+ const reason = flags.reason;
198
+ if (!reason || reason === true) {
199
+ log.error('deliver --force requires --reason="..." when bypassing pending checkpoints');
200
+ process.exitCode = 2;
201
+ return true;
202
+ }
203
+ state.logEvent(st, 'deliver.force_bypass_checkpoint', {
204
+ checkpoint: pending.id,
205
+ type: pending.type,
206
+ reason,
207
+ });
208
+ await state.write(root, slug, st);
209
+ log.warn(`deliver: force-bypassed pending ${pending.type} checkpoint`);
210
+ return false;
211
+ }
212
+
213
+ async function runNotify({ root, slug, st, base, head, detect, flags }) {
214
+ // 1) 提测单.md → reports/submit.md; 测试报告 → reports/test-report.md
215
+ // Overwrite each call so subject/info/test evidence are fresh.
216
+ const env = flags.env || flags.environment || 'test';
217
+ const project = flags.project || (detect && detect.remote && detect.remote.project) || '';
218
+ const build = flags.build || flags.buildNumber || '';
219
+ const reportsDir = await change.ensureSubdir(root, slug, 'reports');
220
+ const submitMd = await submit.renderSubmitDoc({
221
+ root, slug, st, env, base, head, build, detect,
222
+ });
223
+ const submitPath = path.join(reportsDir, 'submit.md');
224
+ await fs.writeFile(submitPath, submitMd, 'utf8');
225
+ log.ok(`submit doc: devflow/changes/${slug}/reports/submit.md`);
226
+
227
+ const testReportMd = await submit.renderTestReportDoc({
228
+ root, slug, st, env, detect,
229
+ });
230
+ const testReportPath = path.join(reportsDir, 'test-report.md');
231
+ await fs.writeFile(testReportPath, testReportMd, 'utf8');
232
+ log.ok(`test report: devflow/changes/${slug}/reports/test-report.md`);
233
+
234
+ // 2) Subject + attachments
235
+ const subject = flags.subject
236
+ || submit.composeSubject({ st, project, env });
237
+
238
+ const attachDefault = flags['no-attach-default'] !== true;
239
+ const attachDocs = flags['attach-docs'] === true || flags.attachDocs === true;
240
+ const attachDesignPlan = flags['attach-design-plan'] === true || flags.attachDesignPlan === true;
241
+ const attachReports = flags['attach-reports'] === true || flags.attachReports === true;
242
+ const attachSubmit = flags['attach-submit'] === true || flags.attachSubmit === true;
243
+ const extraAttach = flags.attach
244
+ ? (Array.isArray(flags.attach) ? flags.attach : [flags.attach])
245
+ : [];
246
+ const attachments = await submit.collectAttachments({
247
+ root, slug,
248
+ includeSubmit: attachSubmit,
249
+ includeTestReport: attachDefault,
250
+ includeDocs: attachDocs,
251
+ includeDesignPlan: attachDesignPlan,
252
+ includeReports: attachReports,
253
+ extra: extraAttach,
254
+ });
255
+
256
+ let notify;
257
+ try { notify = await providers.load(root, { type: 'notify' }); }
258
+ catch (e) {
259
+ log.warn(`notify failed (non-fatal): ${e.message}`);
260
+ return;
261
+ }
262
+ if (!notify || notify.driver === 'local') {
263
+ log.dim('notify: no non-local notify provider configured; skipping');
264
+ return;
265
+ }
266
+
267
+ // 3) Recipients. Command-line flags override provider defaults configured
268
+ // through `devflow provider setup` (`config.defaults.to` / `cc`).
269
+ const { to, cc, source } = resolveNotifyRecipients(flags, notify);
270
+ if (source === 'provider-defaults') {
271
+ log.dim('notify recipients: using notify provider defaults');
272
+ }
273
+
274
+ const dryRun = flags['notify-dry-run'] === true;
275
+ // 4) 人工确认 (arb Step 4) — default ON, --no-confirm to skip, non-TTY refuses by default
276
+ const wantConfirm = flags.confirm !== false && flags['no-confirm'] !== true;
277
+ if (wantConfirm) {
278
+ const ok = await submit.confirmSend({
279
+ subject,
280
+ to, cc,
281
+ attachments,
282
+ body: submitMd,
283
+ });
284
+ if (!ok.ok) {
285
+ log.error(`notify aborted: ${ok.reason}`);
286
+ state.logEvent(st, 'deliver.notify_aborted', { reason: ok.reason });
287
+ process.exitCode = 1;
288
+ return;
289
+ }
290
+ }
291
+
292
+ if (to.length === 0) {
293
+ log.error('notify: --notify-to=<email,...> required');
294
+ process.exitCode = 2;
295
+ return;
296
+ }
297
+
298
+ try {
299
+ const res = await notify.send({
300
+ subject,
301
+ body_md: submitMd,
302
+ to, cc,
303
+ attachments,
304
+ dryRun,
305
+ });
306
+ const outcome = describeNotifyOutcome({ res, notify, dryRun });
307
+ log.ok(outcome.message);
308
+ if (outcome.warning) log.warn(outcome.warning);
309
+ state.logEvent(st, 'deliver.notify', {
310
+ to: res.accepted, id: res.id,
311
+ subject, env, attachments: attachments.map((a) => path.basename(a)),
312
+ dryRun,
313
+ status: outcome.status,
314
+ response: res.response,
315
+ });
316
+ } catch (e) {
317
+ log.warn(`notify failed (non-fatal): ${e.message}`);
318
+ }
319
+ }
320
+
321
+ function parseAddrList(v) {
322
+ if (!v) return [];
323
+ if (Array.isArray(v)) return v.flatMap((x) => parseAddrList(x));
324
+ return String(v).split(/[,;]/).map((s) => s.trim()).filter(Boolean);
325
+ }
326
+
327
+ function resolveNotifyRecipients(flags = {}, notify = {}) {
328
+ const config = notify.config || {};
329
+ const defaults = config.defaults || {};
330
+ const hasToFlag = flags['notify-to'] !== undefined || flags.to !== undefined;
331
+ const hasCcFlag = flags['notify-cc'] !== undefined || flags.cc !== undefined;
332
+ const to = hasToFlag
333
+ ? parseAddrList(flags['notify-to'] || flags.to)
334
+ : parseAddrList(defaults.to || config.defaultTo);
335
+ const cc = hasCcFlag
336
+ ? parseAddrList(flags['notify-cc'] || flags.cc)
337
+ : parseAddrList(defaults.cc || config.defaultCc);
338
+ return {
339
+ to,
340
+ cc,
341
+ source: hasToFlag || hasCcFlag ? 'flags' : 'provider-defaults',
342
+ };
343
+ }
344
+
345
+ function describeNotifyOutcome({ res = {}, notify = {}, dryRun = false }) {
346
+ const accepted = (res.accepted || []).join(', ');
347
+ if (dryRun) {
348
+ return {
349
+ status: 'dry_run',
350
+ message: `notify dry-run composed: ${accepted}`,
351
+ };
352
+ }
353
+
354
+ const config = notify.config || {};
355
+ const host = String(config.host || '').toLowerCase();
356
+ const port = Number(config.port || 0);
357
+ const localHost = host === 'localhost' || host === '127.0.0.1' || host === '::1';
358
+ const localMta = localHost || (port === 25 && !config.user);
359
+ if (localMta) {
360
+ return {
361
+ status: 'queued_by_local_mta',
362
+ message: `notify queued by local SMTP/MTA: ${accepted}`,
363
+ warning: 'local SMTP/sendmail/postfix accepting a message only means it entered the local mail queue; check queue/provider logs to confirm remote delivery.',
364
+ };
365
+ }
366
+
367
+ return {
368
+ status: 'accepted_by_smtp',
369
+ message: `notify accepted by SMTP server: ${accepted}`,
370
+ };
371
+ }
372
+
373
+ module.exports = {
374
+ run,
375
+ runNotify,
376
+ _internals: { parseAddrList, resolveNotifyRecipients, describeNotifyOutcome },
377
+ };