@guanlin4924/devflow-pi 0.1.40 → 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 +609 -42
- 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 +143 -28
- 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 -1
- 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 +13 -4
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/parallel_recover.rb +386 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/parallel_run.rb +28 -2
- 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 +10 -1
- 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/templates/feature-development.yaml +106 -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 +53 -22
|
@@ -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;
|
|
@@ -102,6 +104,12 @@ export async function createEmbeddedRuntime(project: ProjectContext, options: Ru
|
|
|
102
104
|
utils.logger.setTransports({ file: path.join(productHome, "logs") });
|
|
103
105
|
const omp = await import("@oh-my-pi/pi-coding-agent");
|
|
104
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 });
|
|
105
113
|
// Reuse credentials and model discovery from the user's OMP agent, but not
|
|
106
114
|
// its config.yml: a global modelRoles block would override choices made in
|
|
107
115
|
// Devflow's own Roles screen after every restart.
|
|
@@ -134,28 +142,37 @@ export async function createEmbeddedRuntime(project: ProjectContext, options: Ru
|
|
|
134
142
|
} else {
|
|
135
143
|
sessionManager = omp.SessionManager.create(project.root, sessionDir);
|
|
136
144
|
}
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
+
}
|
|
159
176
|
return { omp, runtime, sessionDir };
|
|
160
177
|
}
|
|
161
178
|
|
|
@@ -256,6 +273,15 @@ export async function runInteractiveRuntime(project: ProjectContext, options: Ru
|
|
|
256
273
|
void consumeControlledRollover();
|
|
257
274
|
}, delay);
|
|
258
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?.();
|
|
259
285
|
// A prepared handoff is a durable runtime request, not something that
|
|
260
286
|
// should wait for the user to type another command after a restart.
|
|
261
287
|
await consumeControlledRollover();
|
|
@@ -263,5 +289,10 @@ export async function runInteractiveRuntime(project: ProjectContext, options: Ru
|
|
|
263
289
|
const input = await mode.getUserInput();
|
|
264
290
|
await omp.submitInteractiveInput(mode, runtime.session, input);
|
|
265
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();
|
|
266
297
|
}
|
|
267
298
|
}
|