@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,93 @@
1
+ 'use strict';
2
+ const log = require('../../utils/log.js');
3
+
4
+ const TEXT = `
5
+ @chenguangyao/devflow-kit — independent dev workflow
6
+ command: devflow (short alias: dfk)
7
+
8
+ USAGE
9
+ devflow <command> [subcommand] [options] [args]
10
+
11
+ ENTRY POINTS
12
+ devflow ingest <url|issue-id|incident:id> primary: pull source material, draft proposal
13
+ [--project-root=<dir>[,<dir>]] use primary project git branch for the change slug
14
+ [--follow-links] [--max-links=10] also fetch one-level same-domain/local markdown links
15
+ devflow new <slug> fallback: brainstorm-driven start
16
+ devflow new <slug> --micro L0 path: create plan directly, skip requirement/design/archive
17
+ devflow new <slug> --level=L0 same as --micro
18
+ [--project-root=<dir>[,<dir>]] use primary project git branch for the change slug
19
+
20
+ LIFECYCLE
21
+ devflow propose | devflow requirement | devflow design | devflow plan | devflow apply
22
+ devflow review | devflow test <kind> | devflow verify | devflow deploy | devflow deliver | devflow archive
23
+ devflow requirement --project-root=<dir>[,<dir>] use/override explicitly selected project roots
24
+ devflow requirement --search-root=<dir> auto-route a requirement to projects under a multi-project root
25
+
26
+ SUBSYSTEMS
27
+ devflow provider <list|status|add|import|relogin|...>
28
+ devflow knowledge <init|query|deposit|sync|validate|migrate>
29
+ devflow skills install [--scope=user|project] [--target=...]
30
+ # --scope=user → copy once to ~/.devflow/skills/devflow-kit,
31
+ # then symlink ~/.cursor + ~/.claude + ~/.agents (all projects share)
32
+ # --scope=project → per-repo .cursor/skills + .claude/skills + .agents/skills (default)
33
+ # --copy can force user-scope back to physical copies
34
+ # all shipped skills are grouped under a single devflow-kit/ subfolder,
35
+ # so other skill collections (e.g. ~/.cursor/skills/superpowers/) stay untouched
36
+ devflow skills status # show where skills are installed (and their layout)
37
+ devflow skills migrate [--scope=user|project] # move legacy flat layout into devflow-kit/ subfolder
38
+ devflow skills uninstall [--scope=user|project] # also scrubs any stray flat-layout leftovers
39
+ devflow sync project [--check]
40
+ devflow sync yapi --file=<openapi.json|api.md> [--provider=api.yapi] [--dry-run]
41
+ sync API contract to YAPI through api provider
42
+ devflow logs query [--provider=observability.sls] [--query=...] [--trace-id=...] [--request-id=...]
43
+ query SLS/OSS logs for verify/self-test evidence
44
+ devflow switch project-overview --mode=<reference|supplement|generate>
45
+ devflow enable / devflow disable <feature>
46
+
47
+ OPS
48
+ devflow init [--ide=auto|full|minimal|none]
49
+ initialize devflow in current repo
50
+ (default --ide=auto: skip per-project IDE files
51
+ when ~/.cursor/skills/devflow-kit/ is installed)
52
+ devflow update refresh marker sections
53
+ devflow uninstall [--purge] remove devflow integration
54
+ devflow status current change overview
55
+ devflow status set-level <L0|L1|L2|L3> --reason="..." adjust level; L0 uses micro route
56
+ devflow status risk <add|resolve|accept|list> <type> --reason="..."
57
+ record/close risk signals such as frontend_change, dependency_change, security_sensitive, ci_failure
58
+ devflow doctor [--scope=...] [--fix] [--json] 7-dimension health check
59
+ devflow test remote --preflight --api-base-url=<url> --cmd="..."
60
+ remote API validation against a deployed test env
61
+ devflow test joint --preflight --backend-url=<url> --frontend-url=<url> --cmd="..."
62
+ frontend/backend joint test with browser-tool preflight
63
+ devflow test contract --cmd="..."
64
+ call-chain/API contract consistency validation
65
+ [--split-report] also write legacy reports/<kind>-test.md
66
+ devflow report compact [--remove-split]
67
+ merge existing split reports into reports/test-report.md
68
+ devflow flow <recommend|draft|suggest|add-step|disable-step|move-step|set-verify|diff|picker|apply-selection|card|check|confirm|explain>
69
+ draft and adjust the change workflow from builtin/project recipes
70
+ devflow deploy test --project=<name> trigger configured Jenkins test job
71
+ auto-prompts feature->test merge/push before Jenkins when run from a dev branch
72
+ [--merge-to=test] [--preflight] [--update-self-test]
73
+ non-interactive merge + local compile precheck; opt-in self-test refresh
74
+ devflow deliver --notify email submit body + one reports/test-report.md attachment
75
+ [--attach-design-plan] [--attach-docs] [--attach-reports]
76
+ opt in to design+plan, all docs, or raw report attachments
77
+ devflow checkpoint <show|add|resolve|cancel> [--json]
78
+ structured user decision cards / phase gates
79
+ devflow version | devflow help [topic]
80
+
81
+ LEARN MORE
82
+ See devflow/RFC-001-devflow-kit.md inside the repo, or run "devflow help <command>".
83
+ `;
84
+
85
+ async function run({ topic } = {}) {
86
+ if (topic) {
87
+ log.raw(`(detailed help for "${topic}" not yet written; see RFC for now)`);
88
+ return;
89
+ }
90
+ log.raw(TEXT);
91
+ }
92
+
93
+ module.exports = { run };
@@ -0,0 +1,602 @@
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 change = require('../../core/change.js');
8
+ const config = require('../../core/config.js');
9
+ const state = require('../../core/state.js');
10
+ const routing = require('../../core/project-routing.js');
11
+ const checkpoint = require('../../core/checkpoint.js');
12
+ const workflowInit = require('../../core/workflow-init.js');
13
+ const templates = require('../../templates/index.js');
14
+ const providers = require('../../providers/loader.js');
15
+ const lifecycle = require('../../providers/lifecycle.js');
16
+ const { printWorkflowSelectionGuidance } = require('./_helpers.js');
17
+
18
+ /**
19
+ * Multi-entry router: classifies the input and dispatches to the right provider.
20
+ * - URL (http/https/file) -> intake
21
+ * - PROJ-NUM (e.g. SCRUM-15803) -> issue
22
+ * - incident:<id> / alert:<id> -> ops/incident
23
+ * - else -> error
24
+ */
25
+
26
+ function classify(input) {
27
+ if (!input || typeof input !== 'string') return { kind: 'unknown' };
28
+ if (input.startsWith('http://') || input.startsWith('https://') || input.startsWith('file://')) {
29
+ return { kind: 'url', value: input };
30
+ }
31
+ if (/^[A-Z][A-Z0-9]+-\d+$/.test(input)) {
32
+ return { kind: 'issue', value: input };
33
+ }
34
+ if (/^(incident|alert):/i.test(input)) {
35
+ return { kind: 'incident', value: input.replace(/^[a-z]+:/i, '') };
36
+ }
37
+ if (input.endsWith('.md') || input.startsWith('./') || input.startsWith('../') || input.startsWith('/')) {
38
+ return { kind: 'url', value: input };
39
+ }
40
+ return { kind: 'unknown', value: input };
41
+ }
42
+
43
+ async function run({ positional = [], flags = {}, cwd, _selectProjectIndices } = {}) {
44
+ const root = cwd || process.cwd();
45
+ const input = positional[0];
46
+ if (!input) {
47
+ log.error('usage: devflow ingest <url|issue-id|incident:id>');
48
+ process.exitCode = 2;
49
+ return;
50
+ }
51
+ const { kind, value } = classify(input);
52
+ if (kind === 'unknown') {
53
+ log.error(`cannot classify input: ${input}`);
54
+ log.dim('hint: pass a URL, JIRA-style ID (e.g. SCRUM-15803), or "incident:<id>"');
55
+ process.exitCode = 2;
56
+ return;
57
+ }
58
+
59
+ log.info(`ingest type=${kind} value=${value}`);
60
+
61
+ let provider;
62
+ try {
63
+ provider = await lifecycle.loadOrAuth(root, { type: kind === 'issue' ? 'issue' : 'intake' });
64
+ } catch (e) {
65
+ log.error(e.message);
66
+ if (e.code === 'PROVIDER_NOT_INSTALLED') {
67
+ log.dim(`hint: run "devflow provider setup" to configure an intake provider`);
68
+ log.dim(` or: devflow provider add intake.confluence --type=intake --driver=confluence (built-in, no extra deps)`);
69
+ log.dim(` or: devflow provider add intake.confluence --type=intake --driver=confluence`);
70
+ }
71
+ process.exitCode = 2;
72
+ return;
73
+ }
74
+
75
+ let fetched;
76
+ try {
77
+ fetched = await lifecycle.withAutoRetry(provider, (p) => p.fetch(value));
78
+ } catch (e) {
79
+ log.error(`provider ${provider.name} failed: ${e.message}`);
80
+ process.exitCode = 1;
81
+ return;
82
+ }
83
+
84
+ const linkedRefs = await fetchLinkedRefs(provider, value, fetched.body_md || '', flags);
85
+ const routingQueryParts = [
86
+ fetched.title,
87
+ fetched.body_md,
88
+ ...linkedRefs.items.map((item) => [item.title, item.body_md].join('\n')),
89
+ ];
90
+ if (kind !== 'url') routingQueryParts.push(value);
91
+ const projectRouting = await resolvePreCreateProjectRouting(
92
+ root,
93
+ flags,
94
+ 'ingest',
95
+ routingQueryParts.join('\n'),
96
+ { selectProjectIndices: _selectProjectIndices }
97
+ );
98
+
99
+ // Derive slug
100
+ let slug;
101
+ if (kind === 'issue') {
102
+ slug = change.slugify(value + '-' + fetched.title);
103
+ } else {
104
+ slug = change.slugify(fetched.title || path.basename(value));
105
+ }
106
+ if (flags.slug) slug = change.slugify(flags.slug);
107
+ const branchRoot = projectRouting && projectRouting.slugBranchSource ? projectRouting.slugBranchSource : root;
108
+ slug = change.withCurrentBranchSlug(branchRoot, slug);
109
+
110
+ const level = flags.level || 'L2';
111
+ let info;
112
+ try {
113
+ info = await change.create(root, {
114
+ slug,
115
+ title: fetched.title,
116
+ level,
117
+ source: { type: kind === 'issue' ? 'issue' : 'url', ref: value },
118
+ });
119
+ await persistProjectRouting(root, slug, info.state, projectRouting, 'ingest');
120
+ } catch (e) {
121
+ if (/already exists/.test(e.message)) {
122
+ log.warn(`change already exists: ${slug} (re-using)`);
123
+ info = { slug, dir: change.resolveChangeDir(root, slug) };
124
+ if (projectRouting) {
125
+ try {
126
+ const st = await state.read(root, slug);
127
+ await persistProjectRouting(root, slug, st, projectRouting, 'ingest');
128
+ } catch (readErr) {
129
+ log.warn(`project routing not saved: ${readErr.message}`);
130
+ }
131
+ }
132
+ } else { throw e; }
133
+ }
134
+ printProjectRouting(projectRouting);
135
+
136
+ // refs/<source>-<id>.md
137
+ const refsDir = await change.ensureSubdir(root, slug, 'refs');
138
+ let bodyMd = fetched.body_md || '';
139
+ const savedAssets = await saveImageAttachments(refsDir, fetched.attachments || []);
140
+ if (savedAssets.length) {
141
+ bodyMd = rewriteImageRefs(bodyMd, savedAssets);
142
+ }
143
+ const refName = kind === 'issue' ? `issue-${value}.md` : `intake-${slugFromValue(value)}.md`;
144
+ const refFile = path.join(refsDir, refName);
145
+ await fs.writeFile(refFile, bodyMd, 'utf8');
146
+ log.ok(`refs/${refName}`);
147
+ if (savedAssets.length) log.ok(`refs/assets (${savedAssets.length} image${savedAssets.length === 1 ? '' : 's'})`);
148
+ await writeLinkedRefs(refsDir, linkedRefs);
149
+
150
+ // Draft proposal.md
151
+ const tpl = await templates.load('proposal.md');
152
+ const body = templates.render(tpl, {
153
+ slug,
154
+ title: fetched.title,
155
+ level,
156
+ source: value,
157
+ sourceType: kind,
158
+ sourceRef: value,
159
+ summary: extractSummary(bodyMd),
160
+ date: new Date().toISOString().slice(0, 10),
161
+ });
162
+ await change.writeArtifact(root, slug, 'proposal.md', body);
163
+ log.ok(`proposal: ${path.join(info.dir, 'proposal.md')}`);
164
+ await ensureProblemCardCheckpoint(root, slug, {
165
+ title: fetched.title,
166
+ level,
167
+ source: value,
168
+ refName,
169
+ projectRouting,
170
+ summary: extractSummary(bodyMd),
171
+ });
172
+
173
+ log.raw('');
174
+ log.ok(`change ready: ${slug}`);
175
+ log.dim(`workspace: ${info.dir}`);
176
+ log.dim('next: resolve the problem_card checkpoint, then confirm this change workflow.');
177
+ const draftResult = await createEntryWorkflowDraft(root, slug);
178
+ if (draftResult) {
179
+ printWorkflowSelectionGuidance(slug);
180
+ }
181
+ }
182
+
183
+ async function fetchLinkedRefs(provider, sourceUrl, bodyMd, flags = {}) {
184
+ if (!isTruthy(flags.followLinks || flags['follow-links'])) {
185
+ return { enabled: false, items: [], skipped: [] };
186
+ }
187
+ const requestedDepth = parseInt(flags.linkDepth || flags['link-depth'] || '1', 10);
188
+ if (Number.isFinite(requestedDepth) && requestedDepth > 1) {
189
+ log.warn('ingest linked refs currently supports max depth=1; using 1.');
190
+ }
191
+ const maxLinks = clampInt(flags.maxLinks || flags['max-links'], 10, 0, 20);
192
+ const candidates = extractMarkdownLinks(bodyMd)
193
+ .map((link) => ({ ...link, resolved: resolveIngestLink(sourceUrl, link.href) }))
194
+ .filter((link) => link.resolved);
195
+
196
+ const seen = new Set();
197
+ const items = [];
198
+ const skipped = [];
199
+ for (const link of candidates) {
200
+ const resolved = link.resolved;
201
+ if (seen.has(resolved)) continue;
202
+ seen.add(resolved);
203
+ if (!isAllowedLinkedRef(sourceUrl, resolved)) {
204
+ skipped.push({ label: link.label, url: resolved, reason: '非同域或非本地 markdown 链接' });
205
+ continue;
206
+ }
207
+ if (provider && typeof provider.supports === 'function' && !provider.supports(resolved)) {
208
+ skipped.push({ label: link.label, url: resolved, reason: '当前 intake provider 不支持该链接' });
209
+ continue;
210
+ }
211
+ if (items.length >= maxLinks) {
212
+ skipped.push({ label: link.label, url: resolved, reason: `超过本次链接抓取上限 ${maxLinks}` });
213
+ continue;
214
+ }
215
+ try {
216
+ const fetched = await lifecycle.withAutoRetry(provider, (p) => p.fetch(resolved));
217
+ items.push({
218
+ label: link.label,
219
+ url: resolved,
220
+ title: fetched.title || link.label || resolved,
221
+ body_md: fetched.body_md || '',
222
+ source: fetched.source || { type: 'url', ref: resolved },
223
+ });
224
+ } catch (e) {
225
+ skipped.push({ label: link.label, url: resolved, reason: e.message });
226
+ }
227
+ }
228
+
229
+ if (items.length) log.ok(`linked refs (${items.length})`);
230
+ if (skipped.length) log.warn(`linked refs skipped: ${skipped.length}`);
231
+ return { enabled: true, items, skipped };
232
+ }
233
+
234
+ async function writeLinkedRefs(refsDir, linkedRefs) {
235
+ if (!linkedRefs || !linkedRefs.enabled) return;
236
+ const lines = [
237
+ '# 链接抓取索引',
238
+ '',
239
+ '本文件由 `devflow ingest --follow-links` 自动生成。当前只抓取入口页中的一层同域/本地链接。',
240
+ '',
241
+ '## 已抓取',
242
+ '',
243
+ ];
244
+
245
+ for (const [i, item] of linkedRefs.items.entries()) {
246
+ const n = String(i + 1).padStart(2, '0');
247
+ const base = change.slugify(item.title || item.label || `linked-${n}`) || `linked-${n}`;
248
+ const filename = `linked-${n}-${base}.md`;
249
+ const content = [
250
+ `# ${item.title || item.label || item.url}`,
251
+ '',
252
+ `- 来源链接:${item.url}`,
253
+ item.label ? `- 原文标签:${item.label}` : null,
254
+ '',
255
+ '---',
256
+ '',
257
+ item.body_md || '',
258
+ '',
259
+ ].filter((x) => x !== null).join('\n');
260
+ await fs.writeFile(path.join(refsDir, filename), content, 'utf8');
261
+ lines.push(`- [x] ${item.label || item.title || item.url} -> ${filename}`);
262
+ }
263
+
264
+ if (!linkedRefs.items.length) lines.push('- 无');
265
+ lines.push('', '## 跳过', '');
266
+ for (const item of linkedRefs.skipped || []) {
267
+ lines.push(`- [ ] ${item.label || item.url} (${item.url}):${item.reason}`);
268
+ }
269
+ if (!linkedRefs.skipped.length) lines.push('- 无');
270
+ lines.push('');
271
+ await fs.writeFile(path.join(refsDir, 'link-index.md'), lines.join('\n'), 'utf8');
272
+ log.ok('refs/link-index.md');
273
+ }
274
+
275
+ async function resolvePreCreateProjectRouting(root, flags, source, query, opts = {}) {
276
+ const roots = parseRoots(flags.projectRoot || flags['project-root']);
277
+ if (roots.length) return routing.explicitProjectRouting(roots, { source });
278
+ if (flags['no-project-routing'] || flags.noProjectRouting) return null;
279
+
280
+ const searchRoots = parseRoots(flags.searchRoot || flags['search-root'] || flags.projectsRoot || root);
281
+ const result = await routing.routeProjects({
282
+ searchRoots,
283
+ query,
284
+ maxDepth: parseInt(flags.projectDepth || flags['project-depth'] || '3', 10),
285
+ topK: parseInt(flags.projectTopK || flags['project-top-k'] || '5', 10),
286
+ });
287
+ if (!result.recommended.length) return null;
288
+ const projectRouting = {
289
+ status: 'recommended',
290
+ source,
291
+ searchRoots: result.searchRoots,
292
+ queryTerms: routing.extractTerms(query).slice(0, 20),
293
+ candidates: result.candidates,
294
+ recommended: result.recommended,
295
+ slugBranchSource: result.recommended[0].root,
296
+ };
297
+ return maybeSelectProjectRouting(projectRouting, opts);
298
+ }
299
+
300
+ async function persistProjectRouting(root, slug, st, projectRouting, source) {
301
+ if (!projectRouting || !st) return st;
302
+ st.projectRouting = projectRouting;
303
+ st.projects = projectRouting.recommended;
304
+ await routing.writeProjectRootsArtifact(root, slug, projectRouting, { source });
305
+ state.logEvent(st, 'project routing selected', {
306
+ source,
307
+ status: projectRouting.status,
308
+ projects: st.projects.length,
309
+ });
310
+ await state.write(root, slug, st);
311
+ return st;
312
+ }
313
+
314
+ async function maybeSelectProjectRouting(projectRouting, opts = {}) {
315
+ if (!projectRouting || projectRouting.candidates.length <= 1) return projectRouting;
316
+
317
+ let picks = null;
318
+ if (typeof opts.selectProjectIndices === 'function') {
319
+ picks = await opts.selectProjectIndices(projectRouting.candidates);
320
+ } else if (isTty(opts.stdin || process.stdin, opts.stdout || process.stdout)) {
321
+ picks = await promptProjectSelection(projectRouting.candidates, opts);
322
+ }
323
+ if (!Array.isArray(picks)) return projectRouting;
324
+ if (!picks.length) return null;
325
+ return selectProjectRouting(projectRouting, picks);
326
+ }
327
+
328
+ async function promptProjectSelection(candidates, opts = {}) {
329
+ log.raw('');
330
+ log.info('project routing candidates:');
331
+ for (const [i, p] of candidates.slice(0, 8).entries()) {
332
+ log.raw(` ${i + 1}. ${p.name} confidence=${p.confidence} ${p.root}`);
333
+ for (const e of (p.evidence || []).slice(0, 2)) log.dim(` - ${e}`);
334
+ }
335
+ const answer = await ask('select project(s), e.g. "1" or "1,2" [1]: ', opts);
336
+ return parseProjectSelection(answer, candidates.length);
337
+ }
338
+
339
+ function selectProjectRouting(projectRouting, picks) {
340
+ const candidates = projectRouting.candidates || [];
341
+ const recommended = picks
342
+ .map((idx) => candidates[idx])
343
+ .filter(Boolean)
344
+ .map((p, i) => ({
345
+ name: p.name,
346
+ root: p.root,
347
+ role: i === 0 ? 'primary' : 'dependency',
348
+ branch: p.branch || change.currentBranch(p.root) || null,
349
+ confidence: p.confidence,
350
+ evidence: p.evidence || [],
351
+ }));
352
+ if (!recommended.length) return projectRouting;
353
+ return {
354
+ ...projectRouting,
355
+ status: 'selected',
356
+ recommended,
357
+ slugBranchSource: recommended[0].root,
358
+ };
359
+ }
360
+
361
+ function parseProjectSelection(input, count) {
362
+ const s = String(input || '').trim().toLowerCase();
363
+ if (!s) return [0];
364
+ if (s === 'none' || s === 'no' || s === 'n') return [];
365
+ if (s === 'all' || s === 'a' || s === '*') {
366
+ return Array.from({ length: count }, (_, i) => i);
367
+ }
368
+ const seen = new Set();
369
+ for (const tok of s.split(/[,\s]+/)) {
370
+ const n = parseInt(tok, 10);
371
+ if (Number.isFinite(n) && n >= 1 && n <= count) seen.add(n - 1);
372
+ }
373
+ return Array.from(seen).sort((a, b) => a - b);
374
+ }
375
+
376
+ function ask(question, opts = {}) {
377
+ const input = opts.stdin || process.stdin;
378
+ const output = opts.stdout || process.stdout;
379
+ const rl = readline.createInterface({ input, output });
380
+ return new Promise((resolve) => {
381
+ rl.question(question, (answer) => {
382
+ rl.close();
383
+ resolve(answer);
384
+ });
385
+ });
386
+ }
387
+
388
+ function isTty(stdin, stdout) {
389
+ return Boolean(stdin && stdout && stdin.isTTY && stdout.isTTY);
390
+ }
391
+
392
+ function printProjectRouting(projectRouting) {
393
+ if (!projectRouting || !projectRouting.candidates.length) return;
394
+ log.raw('');
395
+ log.info('project routing recommendations:');
396
+ for (const [i, p] of projectRouting.candidates.slice(0, 5).entries()) {
397
+ const mark = projectRouting.recommended.some((r) => r.root === p.root) ? '*' : ' ';
398
+ log.raw(` ${mark} ${i + 1}. ${p.name} confidence=${p.confidence} ${p.root}`);
399
+ for (const e of (p.evidence || []).slice(0, 3)) log.dim(` - ${e}`);
400
+ }
401
+ log.dim('override if needed: devflow ingest <input> --project-root=<dir>[,<dir>]');
402
+ }
403
+
404
+ function parseRoots(value) {
405
+ return String(value || '')
406
+ .split(',')
407
+ .map((s) => s.trim())
408
+ .filter(Boolean);
409
+ }
410
+
411
+ function slugFromValue(v) {
412
+ return change.slugify(v.replace(/^https?:\/\//, '').replace(/[\/?#].*$/, '')).slice(0, 60);
413
+ }
414
+
415
+ function extractSummary(md) {
416
+ if (!md) return '';
417
+ // Skip first H1 and any frontmatter, take first 5 non-heading paragraphs.
418
+ let body = md.replace(/^---[\s\S]*?\n---\n/, '');
419
+ body = body.replace(/^#\s.+\n+/, '');
420
+ const paras = body.split(/\n\s*\n/).slice(0, 3);
421
+ return paras.join('\n\n').slice(0, 800);
422
+ }
423
+
424
+ async function ensureProblemCardCheckpoint(root, slug, details) {
425
+ const st = await state.read(root, slug);
426
+ if (checkpoint.latestPendingByType(st, 'problem_card')) return null;
427
+ if (checkpoint.hasResolvedDecision(st, 'problem_card', 'accept')) return null;
428
+
429
+ const projectNames = details.projectRouting && details.projectRouting.recommended && details.projectRouting.recommended.length
430
+ ? details.projectRouting.recommended.map((p) => `${p.name}:${p.role || 'candidate'}`).join(', ')
431
+ : '未确认';
432
+ const summary = `${details.title || slug};level=${details.level || st.level || 'L2'};projects=${projectNames}`;
433
+ const evidence = ['proposal.md'];
434
+ if (details.refName) evidence.push(`refs/${details.refName}`);
435
+ const cp = checkpoint.addCheckpoint(st, {
436
+ type: 'problem_card',
437
+ phase: 'intake',
438
+ summary,
439
+ question: '是否接受当前问题卡、复杂度分级和项目范围,并进入 requirement?',
440
+ options: [
441
+ { id: 'accept', label: '接受并进入需求分析', command: 'devflow checkpoint resolve --id=<checkpoint-id> --decision=accept' },
442
+ { id: 'revise', label: '继续修正问题卡', command: '编辑 proposal.md,必要时重跑 devflow ingest --project-root=...' },
443
+ ],
444
+ nextAction: 'devflow checkpoint resolve --id=<checkpoint-id> --decision=accept',
445
+ evidence,
446
+ risks: ['未确认问题卡时直接 requirement,容易把错误范围带入后续设计和实现。'],
447
+ });
448
+ cp.options = cp.options.map((opt) => opt.id === 'accept'
449
+ ? { ...opt, command: `devflow checkpoint resolve --id=${cp.id} --decision=accept` }
450
+ : opt);
451
+ cp.nextAction = `devflow checkpoint resolve --id=${cp.id} --decision=accept`;
452
+ state.logEvent(st, 'checkpoint.add', { id: cp.id, type: cp.type, phase: cp.phase });
453
+ await state.write(root, slug, st);
454
+ log.raw('');
455
+ log.raw(checkpoint.renderNextStepCard(cp));
456
+ return cp;
457
+ }
458
+
459
+ async function createEntryWorkflowDraft(root, slug) {
460
+ try {
461
+ const cfg = (await config.read(root)) || {};
462
+ const st = await state.read(root, slug);
463
+ const result = workflowInit.draftRecommendedWorkflow(root, st, cfg, { auto: true });
464
+ await state.write(root, slug, st);
465
+ if (result.created && result.recipe) {
466
+ log.ok(`workflow draft: ${result.recipe.label} (${result.recipe.id})`);
467
+ }
468
+ return result;
469
+ } catch (e) {
470
+ log.warn(`workflow draft not created: ${e.message}`);
471
+ log.dim(`workflow: devflow flow recommend --slug=${slug}`);
472
+ log.dim(` devflow flow draft --slug=${slug}`);
473
+ return null;
474
+ }
475
+ }
476
+
477
+ function extractMarkdownLinks(markdown) {
478
+ const out = [];
479
+ const re = /(!)?\[([^\]]+)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g;
480
+ let m;
481
+ while ((m = re.exec(markdown || '')) !== null) {
482
+ if (m[1]) continue;
483
+ const label = (m[2] || '').trim();
484
+ const href = (m[3] || '').trim();
485
+ if (!href || href.startsWith('#')) continue;
486
+ if (/^(mailto|javascript|tel):/i.test(href)) continue;
487
+ out.push({ label, href });
488
+ }
489
+ return out;
490
+ }
491
+
492
+ function resolveIngestLink(sourceUrl, href) {
493
+ if (!href) return null;
494
+ if (/^https?:\/\//i.test(href) || href.startsWith('file://')) return href;
495
+ if (/^https?:\/\//i.test(sourceUrl)) {
496
+ try { return new URL(href, sourceUrl).toString(); } catch { return null; }
497
+ }
498
+ let base = sourceUrl || '';
499
+ if (base.startsWith('file://')) base = base.slice(7);
500
+ if (!base) return null;
501
+ return path.resolve(path.dirname(path.resolve(base)), href);
502
+ }
503
+
504
+ function isAllowedLinkedRef(sourceUrl, linkedUrl) {
505
+ if (!linkedUrl) return false;
506
+ if (/^https?:\/\//i.test(sourceUrl) && /^https?:\/\//i.test(linkedUrl)) {
507
+ try {
508
+ const source = new URL(sourceUrl);
509
+ const linked = new URL(linkedUrl);
510
+ return source.origin === linked.origin;
511
+ } catch { return false; }
512
+ }
513
+ if (!/^https?:\/\//i.test(sourceUrl) && !/^https?:\/\//i.test(linkedUrl)) {
514
+ const p = linkedUrl.startsWith('file://') ? linkedUrl.slice(7) : linkedUrl;
515
+ return /\.(md|markdown|txt)$/i.test(p);
516
+ }
517
+ return false;
518
+ }
519
+
520
+ function isTruthy(v) {
521
+ return v === true || v === 'true' || v === '1' || v === 'yes' || v === 'y';
522
+ }
523
+
524
+ function clampInt(v, def, min, max) {
525
+ const n = v === undefined || v === null || v === '' ? def : parseInt(String(v), 10);
526
+ if (!Number.isFinite(n)) return def;
527
+ return Math.max(min, Math.min(max, n));
528
+ }
529
+
530
+ async function saveImageAttachments(refsDir, attachments) {
531
+ const images = (attachments || []).filter((a) => a && a.filename && Buffer.isBuffer(a.data));
532
+ if (!images.length) return [];
533
+ const assetsDir = path.join(refsDir, 'assets');
534
+ await fs.mkdir(assetsDir, { recursive: true });
535
+ const used = new Set();
536
+ const saved = [];
537
+ for (const image of images) {
538
+ const filename = uniqueFilename(safeFilename(image.filename), used);
539
+ const abs = path.join(assetsDir, filename);
540
+ await fs.writeFile(abs, image.data);
541
+ saved.push({
542
+ original: image.filename,
543
+ filename,
544
+ rel: `assets/${filename}`,
545
+ abs,
546
+ });
547
+ }
548
+ return saved;
549
+ }
550
+
551
+ function rewriteImageRefs(markdown, assets) {
552
+ let out = markdown || '';
553
+ for (const asset of assets) {
554
+ const candidates = new Set([
555
+ asset.original,
556
+ encodeURI(asset.original),
557
+ asset.filename,
558
+ encodeURI(asset.filename),
559
+ ]);
560
+ out = out.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (full, alt, src) => {
561
+ const clean = decodeMaybe(src.trim()).split(/[?#]/)[0];
562
+ const base = path.basename(clean);
563
+ if (candidates.has(src.trim()) || candidates.has(clean) || candidates.has(base)) {
564
+ return `![${alt || asset.original}](${asset.rel})`;
565
+ }
566
+ return full;
567
+ });
568
+ }
569
+ return out;
570
+ }
571
+
572
+ function safeFilename(name) {
573
+ const ext = path.extname(name) || '.png';
574
+ const base = path.basename(name, ext)
575
+ .normalize('NFKC')
576
+ .replace(/[\\/:*?"<>|\n\r\t]+/g, '-')
577
+ .replace(/\s+/g, '-')
578
+ .replace(/-+/g, '-')
579
+ .replace(/^-|-$/g, '')
580
+ .slice(0, 80) || 'image';
581
+ return base + ext.toLowerCase();
582
+ }
583
+
584
+ function uniqueFilename(name, used) {
585
+ let candidate = name;
586
+ const ext = path.extname(name);
587
+ const base = path.basename(name, ext);
588
+ let i = 2;
589
+ while (used.has(candidate)) {
590
+ candidate = `${base}-${i}${ext}`;
591
+ i++;
592
+ }
593
+ used.add(candidate);
594
+ return candidate;
595
+ }
596
+
597
+ function decodeMaybe(s) {
598
+ try { return decodeURIComponent(s); }
599
+ catch { return s; }
600
+ }
601
+
602
+ module.exports = { run, classify };