@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.
- package/package.json +1 -1
- package/plugins/devflow-core/extensions/devflow-core.ts +728 -39
- package/plugins/devflow-core/skills/wireframe-proposal/SKILL.md +13 -0
- package/plugins/devflow-core/skills/wireframe-proposal/agents/openai.yaml +4 -0
- package/plugins/devflow-core/skills/wireframe-render/SKILL.md +8 -0
- package/plugins/devflow-core/skills/wireframe-review/SKILL.md +8 -0
- package/plugins/devflow-core/skills/wireframe-validate/SKILL.md +8 -0
- package/plugins/devflow-core/skills/wireframe-validate/agents/openai.yaml +4 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/SKILL.md +16 -1
- package/plugins/devflow-core/skills/workflow-orchestrator/contracts/business-candidate-delta.schema.yaml +41 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/contracts/business-claim.schema.yaml +10 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/contracts/business-model-policy.yaml +51 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/contracts/business-model.schema.yaml +241 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/contracts/business-source-registry.schema.yaml +80 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/contracts/proposal.schema.yaml +44 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/contracts/questions.schema.yaml +14 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/acceptance_test_suite.rb +13 -3
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/advance_state.rb +334 -30
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/business_claim_validate.py +77 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/business_closure_e2e_seed.json +516 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/business_cross_model_check.py +76 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/business_impact.py +51 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/business_invalidation.rb +108 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/business_model_commit.rb +570 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/business_model_gate.rb +89 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/business_model_render.py +131 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/business_model_validate.py +1063 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/business_source_resolver.rb +365 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/checker_registry.rb +2 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/cross_track_consistency_check.rb +94 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/execution_contract.rb +3 -3
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/gate_contract.rb +3 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/gate_runtime.rb +84 -6
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/inspect_state.rb +3 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/machine_evidence.rb +45 -12
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/parallel_recover.rb +386 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/parallel_run.rb +248 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/requirement_graph.rb +139 -14
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/requirement_graph_backfill.rb +143 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/session_rollover.rb +15 -4
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/sync_artifacts.rb +45 -1
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/validate_state.rb +86 -2
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/wireframe_commit.rb +222 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/wireframe_render.py +231 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/wireframe_validate.py +457 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/workflow_gate_contract.rb +94 -2
- package/plugins/devflow-core/skills/workflow-orchestrator/templates/feature-development.yaml +111 -1
- package/plugins/devflow-implementation/agents/implementation-worker.md +6 -0
- package/plugins/devflow-intel/skills/workflow-code-index/scripts/code_index.py +107 -37
- package/plugins/devflow-intel/skills/workflow-code-index/scripts/core/scanner.py +20 -1
- package/plugins/devflow-quality/skills/workflow-acceptance-test-design/SKILL.md +1 -1
- package/src/constants.ts +1 -1
- package/src/runtime.ts +61 -24
package/plugins/devflow-core/skills/workflow-orchestrator/templates/feature-development.yaml
CHANGED
|
@@ -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
|
-
-
|
|
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
|
|
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
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":
|
|
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
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
}
|