@guanlin4924/devflow-pi 0.1.39 → 0.1.47

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 (53) hide show
  1. package/package.json +1 -1
  2. package/plugins/devflow-core/extensions/devflow-core.ts +728 -39
  3. package/plugins/devflow-core/skills/wireframe-proposal/SKILL.md +13 -0
  4. package/plugins/devflow-core/skills/wireframe-proposal/agents/openai.yaml +4 -0
  5. package/plugins/devflow-core/skills/wireframe-render/SKILL.md +8 -0
  6. package/plugins/devflow-core/skills/wireframe-review/SKILL.md +8 -0
  7. package/plugins/devflow-core/skills/wireframe-validate/SKILL.md +8 -0
  8. package/plugins/devflow-core/skills/wireframe-validate/agents/openai.yaml +4 -0
  9. package/plugins/devflow-core/skills/workflow-orchestrator/SKILL.md +16 -1
  10. package/plugins/devflow-core/skills/workflow-orchestrator/contracts/business-candidate-delta.schema.yaml +41 -0
  11. package/plugins/devflow-core/skills/workflow-orchestrator/contracts/business-claim.schema.yaml +10 -0
  12. package/plugins/devflow-core/skills/workflow-orchestrator/contracts/business-model-policy.yaml +51 -0
  13. package/plugins/devflow-core/skills/workflow-orchestrator/contracts/business-model.schema.yaml +241 -0
  14. package/plugins/devflow-core/skills/workflow-orchestrator/contracts/business-source-registry.schema.yaml +80 -0
  15. package/plugins/devflow-core/skills/workflow-orchestrator/contracts/proposal.schema.yaml +44 -0
  16. package/plugins/devflow-core/skills/workflow-orchestrator/contracts/questions.schema.yaml +14 -0
  17. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/acceptance_test_suite.rb +13 -3
  18. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/advance_state.rb +334 -30
  19. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/business_claim_validate.py +77 -0
  20. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/business_closure_e2e_seed.json +516 -0
  21. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/business_cross_model_check.py +76 -0
  22. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/business_impact.py +51 -0
  23. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/business_invalidation.rb +108 -0
  24. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/business_model_commit.rb +570 -0
  25. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/business_model_gate.rb +89 -0
  26. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/business_model_render.py +131 -0
  27. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/business_model_validate.py +1063 -0
  28. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/business_source_resolver.rb +365 -0
  29. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/checker_registry.rb +2 -0
  30. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/cross_track_consistency_check.rb +94 -0
  31. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/execution_contract.rb +3 -3
  32. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/gate_contract.rb +3 -0
  33. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/gate_runtime.rb +84 -6
  34. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/inspect_state.rb +3 -0
  35. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/machine_evidence.rb +45 -12
  36. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/parallel_recover.rb +386 -0
  37. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/parallel_run.rb +248 -0
  38. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/requirement_graph.rb +139 -14
  39. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/requirement_graph_backfill.rb +143 -0
  40. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/session_rollover.rb +15 -4
  41. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/sync_artifacts.rb +45 -1
  42. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/validate_state.rb +86 -2
  43. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/wireframe_commit.rb +222 -0
  44. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/wireframe_render.py +231 -0
  45. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/wireframe_validate.py +457 -0
  46. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/workflow_gate_contract.rb +94 -2
  47. package/plugins/devflow-core/skills/workflow-orchestrator/templates/feature-development.yaml +111 -1
  48. package/plugins/devflow-implementation/agents/implementation-worker.md +6 -0
  49. package/plugins/devflow-intel/skills/workflow-code-index/scripts/code_index.py +107 -37
  50. package/plugins/devflow-intel/skills/workflow-code-index/scripts/core/scanner.py +20 -1
  51. package/plugins/devflow-quality/skills/workflow-acceptance-test-design/SKILL.md +1 -1
  52. package/src/constants.ts +1 -1
  53. package/src/runtime.ts +61 -24
@@ -45,6 +45,7 @@ governance:
45
45
  - slice-independence-check
46
46
  - contract-coverage-check
47
47
  - change-impact-check
48
+ - cross-track-consistency-check
48
49
  acceptance-test-design:
49
50
  - requirement-graph-check
50
51
  - contract-coverage-check
@@ -123,6 +124,10 @@ parallel_windows:
123
124
  checkpoint: CP-2
124
125
  execution_policy:
125
126
  mode: parallel_when_supported
127
+ scheduler:
128
+ one_agent_per_step: true
129
+ max_concurrency: 2
130
+ track_order: declared
126
131
  serial_fallback_order:
127
132
  - backend-tech-spec
128
133
  - api-call-guide
@@ -218,6 +223,30 @@ checkpoints:
218
223
  - approve
219
224
  - needs_fix
220
225
  skip_policy: never
226
+ resume_behavior: "user confirms, then continue with wireframe-proposal"
227
+ CP-WIREFRAME:
228
+ name: Low-fidelity wireframe confirmation
229
+ gate_after: wireframe-review
230
+ producing_step: wireframe-review
231
+ required_outputs:
232
+ - ".planning/features/{feature}/canonical.json"
233
+ - ".planning/features/{feature}/canonical.meta.json"
234
+ - ".planning/features/{feature}/wireframes/preview.md"
235
+ - ".planning/features/{feature}/wireframes/flow.svg"
236
+ required_meta_output: ".planning/features/{feature}/canonical.meta.json"
237
+ required_meta_fields:
238
+ - prototype_fingerprint
239
+ - source_document_fingerprint
240
+ - state_revision
241
+ summary_fields:
242
+ - prototype_fingerprint
243
+ - source_document_fingerprint
244
+ - state_revision
245
+ prompt: "确认页面、元素、动作和平台范围;阻塞问题必须已回答,并允许进入 API 契约设计。"
246
+ confirm_options:
247
+ - approve
248
+ - needs_fix
249
+ skip_policy: never
221
250
  resume_behavior: "user confirms, then continue with api-contract"
222
251
  CP-API:
223
252
  name: API contract confirmation
@@ -482,6 +511,84 @@ steps:
482
511
  traceability_status: { type: string, required: true }
483
512
  checkpoint: CP-1
484
513
 
514
+ - id: wireframe-proposal
515
+ order: 11
516
+ label: Wireframe proposal
517
+ skill: wireframe-proposal
518
+ engine_evidence_gate: enforced
519
+ depends_on:
520
+ - requirement-baseline
521
+ inputs:
522
+ - ".planning/requirements/baseline.md"
523
+ - ".planning/requirements/{feature}/traceability.json"
524
+ - ".planning/features/{feature}/questions.yaml"
525
+ outputs:
526
+ - path: ".planning/features/{feature}/proposal.yaml"
527
+ type: wireframe-proposal
528
+ required: true
529
+ - path: ".planning/features/{feature}/questions.yaml"
530
+ type: wireframe-questions
531
+ required: false
532
+
533
+ - id: wireframe-validate
534
+ order: 12
535
+ label: Validate wireframe proposal
536
+ skill: wireframe-validate
537
+ engine_evidence_gate: enforced
538
+ depends_on:
539
+ - wireframe-proposal
540
+ inputs:
541
+ - ".planning/features/{feature}/proposal.yaml"
542
+ - ".planning/features/{feature}/questions.yaml"
543
+ - ".planning/requirements/{feature}/traceability.json"
544
+ outputs:
545
+ - path: ".planning/features/{feature}/canonical.json"
546
+ type: wireframe-canonical
547
+ required: true
548
+ meta_output:
549
+ path: ".planning/features/{feature}/wireframe-validation.meta.json"
550
+ fields:
551
+ verdict: { type: string, required: true }
552
+ canonical_fingerprint: { type: string, required: false }
553
+ unresolved_question_count: { type: integer, required: true }
554
+
555
+ - id: wireframe-render
556
+ order: 13
557
+ label: Render wireframe previews
558
+ skill: wireframe-render
559
+ engine_evidence_gate: enforced
560
+ depends_on:
561
+ - wireframe-validate
562
+ inputs:
563
+ - ".planning/features/{feature}/canonical.json"
564
+ - ".planning/features/{feature}/canonical.meta.json"
565
+ outputs:
566
+ - path: ".planning/features/{feature}/wireframes/preview.md"
567
+ type: wireframe-preview
568
+ required: true
569
+ - path: ".planning/features/{feature}/wireframes/flow.svg"
570
+ type: wireframe-flow
571
+ required: true
572
+
573
+ - id: wireframe-review
574
+ order: 14
575
+ label: Wireframe review
576
+ skill: wireframe-review
577
+ engine_evidence_gate: enforced
578
+ depends_on:
579
+ - wireframe-render
580
+ inputs:
581
+ - ".planning/features/{feature}/canonical.json"
582
+ - ".planning/features/{feature}/canonical.meta.json"
583
+ - ".planning/features/{feature}/questions.yaml"
584
+ - ".planning/features/{feature}/wireframes/preview.md"
585
+ - ".planning/features/{feature}/wireframes/flow.svg"
586
+ outputs:
587
+ - path: ".planning/features/{feature}/approval.json"
588
+ type: wireframe-approval
589
+ required: false
590
+ checkpoint: CP-WIREFRAME
591
+
485
592
  - id: api-contract
486
593
  order: 15
487
594
  label: API contract
@@ -873,7 +980,7 @@ steps:
873
980
  skill: workflow-generate-mainflow-cases
874
981
  engine_evidence_gate: enforced
875
982
  depends_on:
876
- - code-index-refresh
983
+ - execute
877
984
  inputs:
878
985
  - ".planning/STATE.md"
879
986
  - ".planning/navigation/INDEX.md"
@@ -887,6 +994,9 @@ steps:
887
994
  - ".planning/features/{feature}/PROGRESS.md"
888
995
  - ".planning/requirements/{feature}/tests.json"
889
996
  outputs:
997
+ - path: ".planning/generated-cases/{feature}/codebase-discovery.json"
998
+ type: mainflow-codebase-discovery
999
+ required: true
890
1000
  - path: ".planning/generated-cases/{feature}/source-map.md"
891
1001
  type: mainflow-source-map
892
1002
  required: true
@@ -18,6 +18,12 @@ autoloadSkills:
18
18
 
19
19
  Implement only the assigned plan slice. Do not re-plan the feature, compare alternatives, or explore unrelated modules. Read only the approved files, their immediate dependencies, and the focused tests needed for this slice. Preserve unrelated work and report deviations instead of silently expanding scope.
20
20
 
21
+ Before any protected source access, read the project's required code-index evidence
22
+ files in the exact order listed by INDEX_GATE (typically `.planning/code-index/INDEX.md`
23
+ followed by the ordered files named by the gate). Do not attempt `grep`, `rg`, or source
24
+ reads first: each subagent has its own evidence session and cannot inherit the parent's
25
+ observations. If the gate lists different files, follow that list exactly.
26
+
21
27
  If the execution contract is missing, ambiguous, or contradicted by the code, stop before editing and return `STATUS: escalate` with the conflicting evidence. Run the smallest focused validation once; report failures rather than repeatedly trying speculative fixes.
22
28
 
23
29
  For a task whose frozen `test-bindings.json` marks `test_mode: tdd_required`, use the Core tool in this exact order: `devflow_tdd_evidence(action="prepare")` before changing declared production paths; create/change the test and record `red`; implement; record `green`; then record `regression`. Do not mark the task done if any phase is missing or fails. The tool may write only its own Core-owned TDD evidence record; this exception does not authorize edits to any other Devflow control-plane file.
@@ -5,6 +5,7 @@ import json
5
5
  import os
6
6
  import shutil
7
7
  import sqlite3
8
+ import subprocess
8
9
  import sys
9
10
  import tempfile
10
11
  import uuid
@@ -14,8 +15,6 @@ from typing import Dict, Iterable, List, Set, Tuple
14
15
 
15
16
  import fcntl
16
17
 
17
- sys.dont_write_bytecode = True
18
-
19
18
  SCRIPT_DIR = Path(__file__).resolve().parent
20
19
  if str(SCRIPT_DIR) not in sys.path:
21
20
  sys.path.insert(0, str(SCRIPT_DIR))
@@ -58,13 +57,14 @@ def _index_lock(output: Path, exclusive: bool):
58
57
  fcntl.flock(handle.fileno(), fcntl.LOCK_UN)
59
58
 
60
59
 
61
- def _publish_snapshot(staging: Path, output: Path, counts: Dict[str, int]) -> None:
60
+ def _publish_snapshot(staging: Path, output: Path, counts: Dict[str, int], workspace_state: Dict[str, object]) -> None:
62
61
  generation = str(uuid.uuid4())
63
62
  snapshot = {
64
63
  "schemaVersion": 1,
65
64
  "generation": generation,
66
65
  "generatedAt": datetime.now(timezone.utc).isoformat(),
67
66
  "counts": counts,
67
+ "workspace": workspace_state,
68
68
  "artifacts": [*INDEX_FILES.values(), "index.sqlite", *MARKDOWN_FILES],
69
69
  }
70
70
  snapshot_path = staging / SNAPSHOT_FILE
@@ -72,8 +72,6 @@ def _publish_snapshot(staging: Path, output: Path, counts: Dict[str, int]) -> No
72
72
  with snapshot_path.open("rb") as handle:
73
73
  os.fsync(handle.fileno())
74
74
 
75
- # Readers in this module hold the shared lock. SNAPSHOT.json is published
76
- # last and is therefore the commit marker for external consumers.
77
75
  for name in snapshot["artifacts"]:
78
76
  os.replace(staging / name, output / name)
79
77
  os.replace(snapshot_path, output / SNAPSHOT_FILE)
@@ -84,6 +82,53 @@ def _publish_snapshot(staging: Path, output: Path, counts: Dict[str, int]) -> No
84
82
  os.close(directory_fd)
85
83
 
86
84
 
85
+ def _workspace_state(root: Path, files: List[dict], changed: Set[str]) -> Dict[str, object]:
86
+ indexed_paths = sorted(record["path"] for record in files)
87
+ source_manifest_hash = _hash_lines(f"{record['path']}\0{record['content_hash']}" for record in files)
88
+ return {
89
+ "repositoryRevision": _git_output(root, ["rev-parse", "HEAD"]),
90
+ "contentManifestHash": source_manifest_hash,
91
+ "indexedPathsHash": _hash_lines(indexed_paths),
92
+ "dirtyPathsAtBuild": sorted(changed),
93
+ }
94
+
95
+
96
+ def _worktree_signature(root: Path) -> str:
97
+ try:
98
+ proc = subprocess.run(
99
+ ["git", "status", "--porcelain=v1", "--untracked-files=all"],
100
+ cwd=str(root),
101
+ text=True,
102
+ stdout=subprocess.PIPE,
103
+ stderr=subprocess.DEVNULL,
104
+ check=False,
105
+ )
106
+ except (OSError, FileNotFoundError):
107
+ return ""
108
+ if proc.returncode != 0:
109
+ return ""
110
+ kept = []
111
+ for line in proc.stdout.splitlines():
112
+ path = line[3:].strip() if len(line) > 3 else ""
113
+ if path.startswith(".planning/code-index/"):
114
+ continue
115
+ kept.append(line)
116
+ return "\n".join(kept)
117
+
118
+
119
+ def _git_output(root: Path, args: List[str]) -> str:
120
+ try:
121
+ proc = subprocess.run(["git", *args], cwd=str(root), text=True, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, check=False)
122
+ except (OSError, FileNotFoundError):
123
+ return ""
124
+ return proc.stdout.strip() if proc.returncode == 0 else ""
125
+
126
+
127
+ def _hash_lines(values) -> str:
128
+ digest = uuid.uuid5(uuid.NAMESPACE_URL, "\n".join(values))
129
+ return f"uuid5:{digest}"
130
+
131
+
87
132
  def main(argv=None) -> int:
88
133
  parser = argparse.ArgumentParser(description="Build and validate a generic code intelligence index.")
89
134
  sub = parser.add_subparsers(dest="command", required=True)
@@ -201,6 +246,7 @@ def command_scan(args) -> int:
201
246
  "relations": len(relations),
202
247
  "diagnostics": len(diagnostics),
203
248
  },
249
+ _workspace_state(root, files, changed),
204
250
  )
205
251
  finally:
206
252
  shutil.rmtree(staging, ignore_errors=True)
@@ -212,38 +258,6 @@ def command_scan(args) -> int:
212
258
 
213
259
  def command_validate(args) -> int:
214
260
  output = Path(args.index).resolve()
215
- with _index_lock(output, exclusive=False):
216
- files, symbols, relations, diagnostics = _load_tuple(output)
217
- errors = validate_index(files, symbols, relations, diagnostics)
218
- if errors:
219
- for error in errors:
220
- print(error)
221
- return 1
222
- print("Code index schema validation passed.")
223
- return 0
224
-
225
-
226
- def command_audit(args) -> int:
227
- root = Path(args.root).resolve()
228
- output = Path(args.index).resolve()
229
- with _index_lock(output, exclusive=True):
230
- files, symbols, relations, diagnostics = _load_tuple(output)
231
- findings = []
232
- findings.extend(audit_stale(root, files))
233
- findings.extend(audit_relations(symbols, relations))
234
- skill_root = Path(args.skill_root)
235
- if skill_root.exists():
236
- findings.extend(audit_project_binding([skill_root / "workflow-code-index", skill_root / "code-index-query"]))
237
- write_audit(output, findings)
238
- if findings:
239
- for finding in findings[:200]:
240
- location = f" {finding.get('path', '')}:{finding.get('line', '')}".rstrip(":")
241
- print(f"{finding.get('severity', 'warning')}: {finding.get('kind', 'unknown')}: {finding.get('message', '')}{location}")
242
- return 1
243
- print("Code index audit passed.")
244
- return 0
245
-
246
-
247
261
  def command_query(args) -> int:
248
262
  output = Path(args.index).resolve()
249
263
  with _index_lock(output, exclusive=False):
@@ -251,6 +265,11 @@ def command_query(args) -> int:
251
265
  if not index.exists():
252
266
  print(f"Missing index: {index}", file=sys.stderr)
253
267
  return 1
268
+ snapshot = _load_snapshot(output)
269
+ files = read_jsonl(output / INDEX_FILES["files"])
270
+ if not _query_is_fresh(output.parent.parent, snapshot, files):
271
+ print("Stale code index: refresh required before query.", file=sys.stderr)
272
+ return 1
254
273
  conn = sqlite3.connect(str(index))
255
274
  conn.row_factory = sqlite3.Row
256
275
  try:
@@ -283,6 +302,57 @@ def command_query(args) -> int:
283
302
  return 0
284
303
 
285
304
 
305
+ def _load_snapshot(output: Path) -> Dict[str, object]:
306
+ path = output / SNAPSHOT_FILE
307
+ if not path.exists():
308
+ return {}
309
+ return json.loads(path.read_text(encoding="utf-8"))
310
+
311
+
312
+ def _query_is_fresh(root: Path, snapshot: Dict[str, object], files: List[dict]) -> bool:
313
+ workspace = snapshot.get("workspace") if isinstance(snapshot, dict) else None
314
+ if not isinstance(workspace, dict):
315
+ return False
316
+ if workspace.get("repositoryRevision") and workspace.get("repositoryRevision") != _git_output(root, ["rev-parse", "HEAD"]):
317
+ return False
318
+ indexed_manifest = _hash_lines(f"{record['path']}\0{record['content_hash']}" for record in files)
319
+ if workspace.get("contentManifestHash") != indexed_manifest:
320
+ return False
321
+ return True
322
+
323
+
324
+
325
+ def command_audit(args) -> int:
326
+ root = Path(args.root).resolve()
327
+ output = Path(args.index).resolve()
328
+ with _index_lock(output, exclusive=True):
329
+ files, symbols, relations, diagnostics = _load_tuple(output)
330
+ findings = []
331
+ findings.extend(audit_stale(root, files))
332
+ findings.extend(audit_relations(symbols, relations))
333
+ skill_root = Path(args.skill_root)
334
+ if skill_root.exists():
335
+ findings.extend(audit_project_binding([skill_root / "workflow-code-index", skill_root / "code-index-query"]))
336
+ write_audit(output, findings)
337
+ if findings:
338
+ for finding in findings[:200]:
339
+ location = f" {finding.get('path', '')}:{finding.get('line', '')}".rstrip(":")
340
+ print(f"{finding.get('severity', 'warning')}: {finding.get('kind', 'unknown')}: {finding.get('message', '')}{location}")
341
+ return 1
342
+ print("Code index audit passed.")
343
+ return 0
344
+
345
+
346
+ def _load_snapshot(output: Path) -> Dict[str, object]:
347
+ path = output / SNAPSHOT_FILE
348
+ if not path.exists():
349
+ return {}
350
+ return json.loads(path.read_text(encoding="utf-8"))
351
+
352
+
353
+
354
+
355
+
286
356
  def _load_tuple(output: Path):
287
357
  return (
288
358
  read_jsonl(output / INDEX_FILES["files"]),
@@ -6,6 +6,7 @@ from typing import Iterable, List, Sequence
6
6
 
7
7
  DEFAULT_EXCLUDES = {
8
8
  ".git",
9
+ ".planning",
9
10
  "node_modules",
10
11
  "vendor",
11
12
  "dist",
@@ -59,7 +60,7 @@ TEXT_EXTENSIONS = {
59
60
  def scan_files(root: Path, excludes: Iterable[str] = (), max_bytes: int = 1_500_000) -> List[Path]:
60
61
  root = root.resolve()
61
62
  excluded = DEFAULT_EXCLUDES | set(excludes)
62
- paths = _scan_with_rg(root, excluded) or _scan_with_walk(root, excluded)
63
+ paths = _scan_with_git(root, excluded) or _scan_with_rg(root, excluded) or _scan_with_walk(root, excluded)
63
64
  result: List[Path] = []
64
65
  for path in sorted(paths):
65
66
  if not path.is_file():
@@ -77,6 +78,24 @@ def scan_files(root: Path, excludes: Iterable[str] = (), max_bytes: int = 1_500_
77
78
  return result
78
79
 
79
80
 
81
+ def _scan_with_git(root: Path, excluded: Sequence[str]) -> List[Path]:
82
+ try:
83
+ proc = subprocess.run(
84
+ ["git", "ls-files", "--cached", "--others", "--exclude-standard"],
85
+ cwd=str(root),
86
+ text=True,
87
+ stdout=subprocess.PIPE,
88
+ stderr=subprocess.DEVNULL,
89
+ check=False,
90
+ )
91
+ except (OSError, FileNotFoundError):
92
+ return []
93
+ if proc.returncode != 0:
94
+ return []
95
+ return [root / line.strip() for line in proc.stdout.splitlines() if line.strip()]
96
+
97
+
98
+
80
99
  def _scan_with_rg(root: Path, excluded: Sequence[str]) -> List[Path]:
81
100
  ignore_args = []
82
101
  for name in excluded:
@@ -12,7 +12,7 @@ description: "Use after integration verification and before change planning to d
12
12
  本步骤的图谱写入受 ExecutionContract v2 约束:`devflow_requirement_graph(action="propose", kind="test_case")` 与 `devflow_api_test_matrix(action="import")` 仅在 active contract 的 step 为 `acceptance-test-design` 时放行。步骤开始且无 active contract 时:
13
13
 
14
14
  1. 先调用 `devflow_contract(action="status")` 检查当前会话是否有 active contract;
15
- 2. 未激活时调用 `devflow_contract(action="activate", feature="<feature>", step="acceptance-test-design")` 合成并激活(planning / waiting-user / awaiting-approval 阶段均可,Core 校验 STATE 与图谱 revision 后由用户确认);
15
+ 2. 未激活时调用 `devflow_contract(action="activate", feature="<feature>", step="acceptance-test-design")` 合成并激活(planning / waiting-user / awaiting-approval,或 Runtime 已触发当前步骤的 executing 回合均可;Core 校验 STATE 当前 feature/step 与图谱 revision 后由用户确认);
16
16
  3. 会话中断后 contract 由 runtime 在 `session_start` 时从 `.planning/contracts/{feature}/EC-*.json` 自动恢复;恢复时校验 state/graph revision,推进后自动失效,需重新合成。
17
17
 
18
18
  ## 输入
package/src/constants.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export const PRODUCT_NAME = "Devflow Pi";
2
- export const PRODUCT_VERSION = "0.1.39";
2
+ export const PRODUCT_VERSION = "0.1.47";
3
3
  export const PROJECT_SCHEMA_VERSION = 1;
4
4
  export const OMP_RUNTIME_VERSION = "17.2.7";
5
5
 
package/src/runtime.ts CHANGED
@@ -1,10 +1,12 @@
1
1
  import { mkdir, readFile, rm } from "node:fs/promises";
2
+ import { homedir } from "node:os";
2
3
  import path from "node:path";
3
4
 
4
5
  import { OMP_RUNTIME_VERSION, PRODUCT_NAME, PRODUCT_VERSION } from "./constants.ts";
5
6
  import { createPlatformExtension } from "./platform-extension.ts";
6
7
  import { devflowAgentDir, devflowHome, pluginRoots, projectSessionDir, sharedOmpAgentDir } from "./paths.ts";
7
8
  import type { ProjectContext } from "./project-context.ts";
9
+ import { injectOmpExtensionCliRoots } from "@oh-my-pi/pi-coding-agent/discovery/omp-extension-roots";
8
10
 
9
11
  export interface RuntimeOptions {
10
12
  initialPrompt?: string;
@@ -36,12 +38,18 @@ const MANAGED_SETTINGS: Record<string, unknown> = {
36
38
  "skills.enableAgentsProject": false,
37
39
  // The interactive session is the high-capability Planner. Low-cost agents
38
40
  // are dispatched only through explicit Devflow steps, one at a time by
39
- // default, so background work cannot silently widen a user's request.
41
+ // default, so background work cannot silently widen a user's request. The
42
+ // parallel workflow window is explicitly capped at two workers, matching
43
+ // its two declared tracks; serial workflows are unaffected.
40
44
  // Keep ordinary interactive turns on the user's DEFAULT role. In particular,
41
45
  // do not inherit SLOW just because a thinking model is configured.
42
46
  "defaultThinkingLevel": "low",
47
+ // Parallel Devflow orchestration relies on OMP's AsyncJobManager and
48
+ // subagent lifecycle channel. Keep this explicit instead of inheriting the
49
+ // SDK default so a global OMP config cannot silently disable it.
50
+ "async.enabled": true,
43
51
  "task.batch": false,
44
- "task.maxConcurrency": 1,
52
+ "task.maxConcurrency": 2,
45
53
  "task.maxRecursionDepth": 1,
46
54
  "task.enableLsp": true,
47
55
  // Keep cheap workers bounded by role prompts. OMP's loop guards remain
@@ -96,6 +104,12 @@ export async function createEmbeddedRuntime(project: ProjectContext, options: Ru
96
104
  utils.logger.setTransports({ file: path.join(productHome, "logs") });
97
105
  const omp = await import("@oh-my-pi/pi-coding-agent");
98
106
  const roots = pluginRoots();
107
+ // Task workers perform a fresh agent discovery after session creation. The
108
+ // SDK's temporary extension-root scope does not cover that later lookup,
109
+ // so register Devflow package roots as durable CLI roots for this process.
110
+ // Without this, packaged agents such as implementation-worker appear during
111
+ // startup but fail with Unknown agent when a parallel task is dispatched.
112
+ injectOmpExtensionCliRoots(roots, homedir(), project.root, { mode: "explicit-only", replace: true });
99
113
  // Reuse credentials and model discovery from the user's OMP agent, but not
100
114
  // its config.yml: a global modelRoles block would override choices made in
101
115
  // Devflow's own Roles screen after every restart.
@@ -128,28 +142,37 @@ export async function createEmbeddedRuntime(project: ProjectContext, options: Ru
128
142
  } else {
129
143
  sessionManager = omp.SessionManager.create(project.root, sessionDir);
130
144
  }
131
- const runtime = await omp.createAgentSession({
132
- cwd: project.root,
133
- agentDir: productAgentDir,
134
- settings,
135
- authStorage,
136
- modelRegistry,
137
- sessionManager,
138
- additionalExtensionPaths: roots,
139
- disableExtensionDiscovery: true,
140
- extensions: [
141
- createPlatformExtension({
142
- sessionRoot: sessionDir,
143
- ompSessionRoot: path.join(sharedAgentDir, "sessions"),
144
- projectId: project.manifest.projectId,
145
- getConfiguredRole: role => settings.getModelRole(role),
146
- }),
147
- ],
148
- hasUI: options.hasUI ?? true,
149
- enableMCP: options.enableMCP ?? false,
150
- enableLsp: options.enableLsp ?? true,
151
- appendSystemPrompt: TOOL_CALL_SAFETY_PROMPT,
152
- });
145
+ const previousCliResumeSignal = process.env.DEVFLOW_CLI_SESSION_RESUME;
146
+ if (options.resume) process.env.DEVFLOW_CLI_SESSION_RESUME = "1";
147
+ else delete process.env.DEVFLOW_CLI_SESSION_RESUME;
148
+ let runtime;
149
+ try {
150
+ runtime = await omp.createAgentSession({
151
+ cwd: project.root,
152
+ agentDir: productAgentDir,
153
+ settings,
154
+ authStorage,
155
+ modelRegistry,
156
+ sessionManager,
157
+ additionalExtensionPaths: roots,
158
+ disableExtensionDiscovery: true,
159
+ extensions: [
160
+ createPlatformExtension({
161
+ sessionRoot: sessionDir,
162
+ ompSessionRoot: path.join(sharedAgentDir, "sessions"),
163
+ projectId: project.manifest.projectId,
164
+ getConfiguredRole: role => settings.getModelRole(role),
165
+ }),
166
+ ],
167
+ hasUI: options.hasUI ?? true,
168
+ enableMCP: options.enableMCP ?? false,
169
+ enableLsp: options.enableLsp ?? true,
170
+ appendSystemPrompt: TOOL_CALL_SAFETY_PROMPT,
171
+ });
172
+ } finally {
173
+ if (previousCliResumeSignal === undefined) delete process.env.DEVFLOW_CLI_SESSION_RESUME;
174
+ else process.env.DEVFLOW_CLI_SESSION_RESUME = previousCliResumeSignal;
175
+ }
153
176
  return { omp, runtime, sessionDir };
154
177
  }
155
178
 
@@ -250,6 +273,15 @@ export async function runInteractiveRuntime(project: ProjectContext, options: Ru
250
273
  void consumeControlledRollover();
251
274
  }, delay);
252
275
  };
276
+ // A control-plane request may be created by an extension input handler that
277
+ // marks the message handled, so it is not guaranteed to pass through the
278
+ // normal submitInteractiveInput return path. Keep a small runtime-owned
279
+ // poller alive for the lifetime of the interactive loop; it only touches the
280
+ // filesystem when a durable rollover request exists.
281
+ const rolloverPoller = setInterval(() => {
282
+ void consumeControlledRollover();
283
+ }, 500);
284
+ rolloverPoller.unref?.();
253
285
  // A prepared handoff is a durable runtime request, not something that
254
286
  // should wait for the user to type another command after a restart.
255
287
  await consumeControlledRollover();
@@ -257,5 +289,10 @@ export async function runInteractiveRuntime(project: ProjectContext, options: Ru
257
289
  const input = await mode.getUserInput();
258
290
  await omp.submitInteractiveInput(mode, runtime.session, input);
259
291
  await consumeControlledRollover();
292
+ // The submitted turn may create a durable rollover request after the
293
+ // immediate check above returns. Poll once while the UI is waiting for the
294
+ // next user input so a successful step does not require a dummy message to
295
+ // trigger the fresh-session handoff.
296
+ scheduleControlledRolloverRetry();
260
297
  }
261
298
  }