@zq-silk/yui 0.15.11 → 0.15.12
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/ARCHITECTURE.md +8 -4
- package/ARCHITECTURE.zh-CN.md +5 -2
- package/README.md +13 -5
- package/dist/agentRun/agentRun.js +3 -0
- package/dist/cli/commandCatalog.js +38 -10
- package/dist/cli/interactionPolicy.js +4 -0
- package/dist/cli/managedDiagnostics.js +1 -1
- package/dist/cli.js +46 -15
- package/dist/commands/executionAuditCommands.js +10 -0
- package/dist/commands/globalRoleCommands.js +27 -2
- package/dist/commands/projectCommands.js +44 -15
- package/dist/commands/taskCommands.js +95 -33
- package/dist/commands/taskIntegrationCommands.js +3 -1
- package/dist/commands/taskOverviewCommand.js +4 -3
- package/dist/commands/taskPublicationAdoptCommand.js +127 -0
- package/dist/commands/taskPublicationCommands.js +11 -2
- package/dist/commands/taskPublicationVerifyCommand.js +23 -39
- package/dist/commands/taskRemoteDeliveryCommand.js +18 -7
- package/dist/context/runContextPack.js +3 -0
- package/dist/context/taskCatalog.js +187 -0
- package/dist/context/taskContext.js +17 -4
- package/dist/controller/controller.js +11 -2
- package/dist/controller/fileSchedulerStoreAdapter.js +80 -19
- package/dist/controller/globalInputDelivery.js +13 -0
- package/dist/controller/providerRetryAdmission.js +100 -0
- package/dist/controller/providerRetryDelivery.js +218 -0
- package/dist/controller/runtime.js +36 -1
- package/dist/integration/gitIntegrationService.js +19 -6
- package/dist/lifecycle/exactRunTerminalization.js +4 -1
- package/dist/message/globalProviderRetry.js +15 -0
- package/dist/observability/executionAudit.js +19 -0
- package/dist/repository/gitWorkspace.js +371 -105
- package/dist/repository/projectMaintenanceLock.js +75 -18
- package/dist/repository/taskWorkspaceCoordinator.js +71 -124
- package/dist/repository/taskWorkspacePreparer.js +86 -24
- package/dist/repository/workspaceCleanupInspection.js +187 -0
- package/dist/runtime/agentError.js +5 -3
- package/dist/runtime/agentHost.js +28 -11
- package/dist/runtime/builtinAgentErrorMappers.js +91 -0
- package/dist/runtime/codexAppServerRuntime.js +34 -3
- package/dist/runtime/providerControl.js +5 -1
- package/dist/runtime/providerRetry.js +198 -0
- package/dist/runtime/providerRuntimeIdentity.js +28 -2
- package/dist/runtime/sessionTokenMetrics.js +15 -5
- package/dist/runtime/structuredProviderHost.js +6 -2
- package/dist/runtime/taskUsageMetrics.js +275 -0
- package/dist/scheduler/activeRoleRunDelivery.js +12 -0
- package/dist/scheduler/leaderWakeupProcessor.js +5 -0
- package/dist/scheduler/taskExecutionProjection.js +26 -5
- package/dist/scheduler/taskObservabilityProjection.js +6 -44
- package/dist/storage/sqliteSchema.js +31 -0
- package/dist/storage/sqliteStore.js +17 -0
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/storeRpc.js +1 -0
- package/dist/storage/taskCatalog.js +123 -0
- package/dist/storage/taskStore.js +2 -0
- package/dist/task/archiveDiagnostics.js +1 -0
- package/dist/task/archivePreflight.js +124 -0
- package/dist/task/publicationAdoption.js +56 -0
- package/dist/task/publicationReference.js +10 -0
- package/dist/task/remoteDelivery.js +31 -16
- package/dist/web/assets/client/app.js +92 -17
- package/dist/web/assets/client/components.js +55 -13
- package/dist/web/assets/client/i18n.js +72 -4
- package/dist/web/assets/client/taskSurface.js +2 -1
- package/dist/web/assets/client/view.js +35 -7
- package/dist/web/assets/shell.js +6 -0
- package/dist/web/assets/styles/layout.js +7 -0
- package/dist/web/webServer.js +14 -3
- package/dist/web/webSnapshot.js +12 -3
- package/dist/workspace/cleanupInspection.js +63 -0
- package/dist/workspace/workItemChangeSetManager.js +110 -50
- package/docs/agent-result-consumption.md +4 -0
- package/docs/agent-result-consumption.zh-CN.md +3 -0
- package/docs/agent-runtime-drivers.md +7 -0
- package/docs/agent-runtime-drivers.zh-CN.md +5 -0
- package/docs/architecture/README.md +2 -0
- package/docs/architecture/README.zh-CN.md +3 -1
- package/docs/architecture/capabilities-and-resources.md +30 -5
- package/docs/architecture/capabilities-and-resources.zh-CN.md +23 -3
- package/docs/managed-turn-and-session-runtime.md +47 -0
- package/docs/managed-turn-and-session-runtime.zh-CN.md +40 -0
- package/docs/observability/README.md +62 -0
- package/docs/observability/README.zh-CN.md +47 -0
- package/docs/project-refresh.md +77 -0
- package/docs/project-refresh.zh-CN.md +59 -0
- package/docs/provider-retry.md +70 -0
- package/docs/task-delivery.md +133 -13
- package/docs/task-delivery.zh-CN.md +99 -10
- package/docs/task-discovery.md +102 -0
- package/docs/task-discovery.zh-CN.md +86 -0
- package/docs/testing/verification-levels.md +16 -0
- package/docs/testing/verification-levels.zh-CN.md +13 -1
- package/i18n/README.zh-CN.md +13 -7
- package/package.json +1 -1
- package/skills/yui-leader/references/execution.md +3 -2
- package/skills/yui-operator/SKILL.md +13 -2
- package/skills/yui-reviewer/SKILL.md +4 -0
- package/skills/yui-runtime/SKILL.md +17 -2
- package/skills/yui-runtime/references/publication.md +26 -4
- package/skills/yui-runtime/references/recovery.md +24 -0
package/ARCHITECTURE.md
CHANGED
|
@@ -169,8 +169,9 @@ Leader management is limited to its Task; executable code still requires exact
|
|
|
169
169
|
Operator-issued grants. Plugin validation is not a security certification.
|
|
170
170
|
|
|
171
171
|
CLI contributions and controlled Web panels are projections of the Registry.
|
|
172
|
-
Web is loopback-only and Controller-owned
|
|
173
|
-
|
|
172
|
+
Web is loopback-only and Controller-owned. Its authenticated local-user controls
|
|
173
|
+
share CLI domain operations; query panels remain read-only and cannot borrow
|
|
174
|
+
that user authority for mutations or plugin management. See [Plugin SDK](docs/plugin-sdk.md) and
|
|
174
175
|
[capabilities and resources](docs/architecture/capabilities-and-resources.md).
|
|
175
176
|
|
|
176
177
|
## Persistence, completion and operation
|
|
@@ -181,8 +182,11 @@ Homes; malformed state is diagnosed, not automatically repaired.
|
|
|
181
182
|
|
|
182
183
|
Completion freezes the delivery result and checks applicable acceptance,
|
|
183
184
|
integration and review contracts. It is distinct from publication, verified
|
|
184
|
-
remote merge, physical quiescence and archive.
|
|
185
|
-
and clean removable managed resources
|
|
185
|
+
remote merge, physical quiescence and archive. Ordinary archive requires settled
|
|
186
|
+
work and clean removable managed resources. Explicitly authorized force archive
|
|
187
|
+
can retain unresolved evidence and unsafe resources without claiming delivery or
|
|
188
|
+
quiescence. Both preserve Task history and cannot reopen.
|
|
189
|
+
See [Task delivery and archive](docs/task-delivery.md#archive).
|
|
186
190
|
|
|
187
191
|
Runtime health and cost are observations, not semantic verdicts. The Agent reads
|
|
188
192
|
exact faults and current intent to choose retry, repair or abandonment. Yui does
|
package/ARCHITECTURE.zh-CN.md
CHANGED
|
@@ -132,7 +132,8 @@ Registry 暴露 context、消息、artifact、环境、插件以及部分 Task/J
|
|
|
132
132
|
由 Operator 签发的 grant。插件验证不是安全认证。
|
|
133
133
|
|
|
134
134
|
CLI 贡献和受控的 Web 面板是 Registry 的投影。Web 仅本地回环、由 Controller 拥有;浏览器
|
|
135
|
-
|
|
135
|
+
凭据认证本地用户控制,这些控制复用 CLI 领域操作;查询面板仍只读,不能借用该用户权限
|
|
136
|
+
执行修改或插件管理。参见[插件 SDK](docs/plugin-sdk.zh-CN.md)和
|
|
136
137
|
[能力与资源](docs/architecture/capabilities-and-resources.zh-CN.md)。
|
|
137
138
|
|
|
138
139
|
## 持久化、完成与运维
|
|
@@ -141,7 +142,9 @@ Home 有一条只追加的存储迁移链。普通运行时只接受当前记录
|
|
|
141
142
|
有效 Home;畸形状态被诊断,而不是自动修复。
|
|
142
143
|
|
|
143
144
|
完成会冻结交付结果,并检查适用的验收、集成和审查合同。它区别于发布、已验证的远程合并、
|
|
144
|
-
|
|
145
|
+
物理静止和归档。普通归档要求工作已了结、受管资源干净可移除。明确授权的 force 归档
|
|
146
|
+
可以保留未解决证据与不安全资源,但不宣称交付或物理静止。两者都保留 Task 历史且不可
|
|
147
|
+
重开。参见[Task 交付与归档](docs/task-delivery.zh-CN.md#归档)。
|
|
145
148
|
|
|
146
149
|
运行时健康度和成本是观察,而不是语义判定。Agent 读取精确的故障和当前意图,以选择重试、
|
|
147
150
|
修复或放弃。Yui 不会自动创建救援 Worker 或选择另一个模型。
|
package/README.md
CHANGED
|
@@ -30,7 +30,7 @@ not from terminal windows you juggle or details you have to remember.
|
|
|
30
30
|
- **Bring your own Agent** — Codex CLI, Claude Code CLI and ACP peers run behind
|
|
31
31
|
one boundary and stay replaceable without losing the Task.
|
|
32
32
|
- **Local-first and private** — everything runs on your machine for one trusted
|
|
33
|
-
user;
|
|
33
|
+
user; Web is loopback-only, with read-only views and authenticated user controls.
|
|
34
34
|
- **Isolated by default** — repository work happens in managed Git worktrees;
|
|
35
35
|
the stable checkout stays read-only.
|
|
36
36
|
|
|
@@ -137,7 +137,11 @@ when necessary. A failed process does not erase the Task, and an uncertain
|
|
|
137
137
|
submission is not silently repeated.
|
|
138
138
|
|
|
139
139
|
For a visual overview, run `yui web` in another terminal. The local Web view
|
|
140
|
-
shows the same tasks and pending questions
|
|
140
|
+
shows the same tasks and pending questions and lets you send messages, answer
|
|
141
|
+
questions and explicitly queue, steer or interrupt Task input. These authenticated
|
|
142
|
+
Task controls use the same operations as the CLI; Web is not a separate task system.
|
|
143
|
+
See [Web permissions](docs/architecture/capabilities-and-resources.md#cli-and-web)
|
|
144
|
+
and [input timing](docs/managed-turn-and-session-runtime.md#input-timing-queue-steer-and-interrupt).
|
|
141
145
|
|
|
142
146
|
## Architecture
|
|
143
147
|
|
|
@@ -243,7 +247,7 @@ the Controller are what touch the store:
|
|
|
243
247
|
it moves work and records facts — it never judges an answer
|
|
244
248
|
|
|
245
249
|
Agents work in Projects: read-only checkout + isolated worktrees.
|
|
246
|
-
Web
|
|
250
|
+
Web (yui web): loopback views + authenticated user controls.
|
|
247
251
|
```
|
|
248
252
|
|
|
249
253
|
### Layered design
|
|
@@ -253,7 +257,7 @@ never a fixed workflow:
|
|
|
253
257
|
|
|
254
258
|
```text
|
|
255
259
|
Experience — how you interact
|
|
256
|
-
CLI (Operator) · Web (loopback,
|
|
260
|
+
CLI (Operator) · Web (loopback, authenticated) · native Agent sessions
|
|
257
261
|
collect input · show facts · confirm actions · invoke capabilities
|
|
258
262
|
▼
|
|
259
263
|
Intelligence — who decides
|
|
@@ -290,9 +294,13 @@ extra states:
|
|
|
290
294
|
WorkItem open ─▶ accepted ─▶ retired
|
|
291
295
|
|
|
292
296
|
Draft holds planning only; activation adopts a delivery workspace.
|
|
293
|
-
|
|
297
|
+
Ordinary archive needs settled work and clean worktrees; it cannot reopen.
|
|
294
298
|
```
|
|
295
299
|
|
|
300
|
+
Explicitly authorized force archive can retain unresolved evidence and unsafe
|
|
301
|
+
resources; it does not prove delivery or permission to delete them.
|
|
302
|
+
See the [archive contract](docs/task-delivery.md#archive).
|
|
303
|
+
|
|
296
304
|
## Design principles
|
|
297
305
|
|
|
298
306
|
### Agents make decisions; Yui makes work durable
|
|
@@ -2,6 +2,7 @@ import { validateEffectiveLaunchSnapshot } from "../executor/effectiveLaunch.js"
|
|
|
2
2
|
import { validateTaskRecordReference } from "../task/taskRecordReference.js";
|
|
3
3
|
import { validateManagedWorkspace } from "../worktree/managedWorkspace.js";
|
|
4
4
|
import { createRunInput, createRunInputEnvelope, validateRunInput } from "../context/runInputContract.js";
|
|
5
|
+
import { providerRetryProjection } from "../runtime/providerRetry.js";
|
|
5
6
|
import { boundedRunFailureDiagnostic, MAX_RUN_FAILURE_DIAGNOSTIC_BYTES, MAX_RUN_RESULT_OUTPUT_BYTES } from "../domain/agentResultTransport.js";
|
|
6
7
|
export { boundedRunFailureDiagnostic, MAX_RUN_FAILURE_DIAGNOSTIC_BYTES, MAX_RUN_RESULT_OUTPUT_BYTES, transportAgentResult } from "../domain/agentResultTransport.js";
|
|
7
8
|
/** Record lifecycle and observed delivery are separate read-only facts. */
|
|
@@ -17,6 +18,8 @@ export function runExecutionObservation(run, binding, events = []) {
|
|
|
17
18
|
return {
|
|
18
19
|
recordStatus: run.status,
|
|
19
20
|
delivery,
|
|
21
|
+
retry: binding?.retry?.previousRunId === run.id || binding?.retry?.successorRunId === run.id
|
|
22
|
+
? providerRetryProjection(binding) : null,
|
|
20
23
|
...(current === undefined ? {} : {
|
|
21
24
|
attemptId: current.attemptId, observedAt: current.updatedAt,
|
|
22
25
|
...(current.nativeTurnId === undefined ? {} : { nativeTurnId: current.nativeTurnId })
|
|
@@ -215,6 +215,11 @@ const globalSessionChildren = [
|
|
|
215
215
|
summary: "Reconcile durable Session owners with native sessions.",
|
|
216
216
|
usage: "yui session reconcile [--report] [--cleanup]",
|
|
217
217
|
options: ["--report", "--cleanup"]
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
name: "retry",
|
|
221
|
+
summary: "Inspect or control bounded Provider recovery for this Global Session.",
|
|
222
|
+
usage: "yui session retry <role> [show|cancel|disable|enable]"
|
|
218
223
|
}
|
|
219
224
|
];
|
|
220
225
|
const profileChildren = [
|
|
@@ -373,9 +378,9 @@ const taskChildren = [
|
|
|
373
378
|
},
|
|
374
379
|
{
|
|
375
380
|
name: "list",
|
|
376
|
-
summary: "List
|
|
377
|
-
usage: "yui task list [--all] [--verbose]",
|
|
378
|
-
options: ["--all", "--verbose"]
|
|
381
|
+
summary: "List Task overviews or a bounded discovery catalog.",
|
|
382
|
+
usage: "yui task list [--all] [--verbose] | --view compact [--all] [--status <status>] [--project <id>] [--search <text>] [--attention <category>] [--limit <1..100>] [--cursor <cursor>]",
|
|
383
|
+
options: ["--all", "--verbose", "--view", "--status", "--project", "--search", "--attention", "--limit", "--cursor"]
|
|
379
384
|
},
|
|
380
385
|
{ name: "show", summary: "Show a Task.", usage: "yui task show <id>" },
|
|
381
386
|
{
|
|
@@ -406,6 +411,12 @@ const taskChildren = [
|
|
|
406
411
|
usage: "yui task remote-delivery <task> [--json]",
|
|
407
412
|
options: ["--json"]
|
|
408
413
|
},
|
|
414
|
+
{
|
|
415
|
+
name: "archive-preflight",
|
|
416
|
+
summary: "Inspect current archive and exact-owner cleanup blockers without changing state or authorizing removal.",
|
|
417
|
+
usage: "yui task archive-preflight <id> (--integrated|--abandon) [--force] [--json]",
|
|
418
|
+
options: ["--integrated", "--abandon", "--force", "--json"]
|
|
419
|
+
},
|
|
409
420
|
{
|
|
410
421
|
name: "archive",
|
|
411
422
|
summary: "Archive a terminal Task; explicit --force commits despite delivery/cleanup warnings, retaining unsafe resources.",
|
|
@@ -606,17 +617,29 @@ const taskChildren = [
|
|
|
606
617
|
{
|
|
607
618
|
name: "publication",
|
|
608
619
|
summary: "Create or update external PR/MR publication evidence for a Task.",
|
|
609
|
-
sections: [{ id: "manage", title: "Commands", entries: ["upsert", "verify", "list", "show"] }],
|
|
620
|
+
sections: [{ id: "manage", title: "Commands", entries: ["upsert", "diff", "adopt", "verify", "list", "show"] }],
|
|
610
621
|
children: [
|
|
611
622
|
{
|
|
612
623
|
name: "upsert",
|
|
613
624
|
summary: "Create or immutably update an external PR/MR and its publication state.",
|
|
614
|
-
usage: "yui task publication upsert <task> --project <project> --provider <github|gitlab> --repository <owner/name> --kind <pull-request|merge-request> --id <external-id> [--url <url>] [--title <text>] [--source-branch <branch>] [--target-branch <branch>] [--local-commit <sha>] [--remote-commit <sha>] [--state <open|merged|closed>] [--reported|--verified] [--evidence <text>] [--merged-at <iso-timestamp>]",
|
|
615
|
-
options: ["--project", "--provider", "--repository", "--kind", "--id", "--url", "--title", "--source-branch", "--target-branch", "--local-commit", "--remote-commit", "--state", "--reported", "--verified", "--evidence", "--merged-at"]
|
|
625
|
+
usage: "yui task publication upsert <task> --project <project> --provider <github|gitlab> --repository <owner/name> --kind <pull-request|merge-request> --id <external-id> [--url <url>] [--title <text>] [--source-branch <branch>] [--target-branch <branch>] [--local-commit <sha>] [--head-commit <sha>] [--remote-commit <sha>] [--state <open|merged|closed>] [--reported|--verified] [--evidence <text>] [--merged-at <iso-timestamp>]",
|
|
626
|
+
options: ["--project", "--provider", "--repository", "--kind", "--id", "--url", "--title", "--source-branch", "--target-branch", "--local-commit", "--head-commit", "--remote-commit", "--state", "--reported", "--verified", "--evidence", "--merged-at"]
|
|
627
|
+
},
|
|
628
|
+
{
|
|
629
|
+
name: "diff",
|
|
630
|
+
summary: "Read a completed Task's fixed acceptance-to-publication candidate diff using local Git only.",
|
|
631
|
+
usage: "yui task publication diff <task>/<publication> [--integration <id>]",
|
|
632
|
+
options: ["--integration"]
|
|
633
|
+
},
|
|
634
|
+
{
|
|
635
|
+
name: "adopt",
|
|
636
|
+
summary: "Explicitly accept the reviewed publication candidate as covering the original completion.",
|
|
637
|
+
usage: "yui task publication adopt <task>/<publication> --reviewed-diff <sha256> --acceptance <text> [--integration <id>]",
|
|
638
|
+
options: ["--reviewed-diff", "--acceptance", "--integration"]
|
|
616
639
|
},
|
|
617
640
|
{
|
|
618
641
|
name: "verify",
|
|
619
|
-
summary: "
|
|
642
|
+
summary: "Observe the current PR/MR head and merge through its provider; record verification separately from Task coverage.",
|
|
620
643
|
usage: "yui task publication verify (<task>/<publication-id> | <task> <publication-id>)"
|
|
621
644
|
},
|
|
622
645
|
{
|
|
@@ -668,8 +691,13 @@ const taskChildren = [
|
|
|
668
691
|
name: "session",
|
|
669
692
|
summary: "Inspect, stop or explicitly select a new Task Role Session.",
|
|
670
693
|
executable: true,
|
|
671
|
-
sections: [{ id: "manage", title: "Commands", entries: ["inspect", "stop", "new"] }],
|
|
694
|
+
sections: [{ id: "manage", title: "Commands", entries: ["inspect", "retry", "stop", "new"] }],
|
|
672
695
|
children: [
|
|
696
|
+
{
|
|
697
|
+
name: "retry",
|
|
698
|
+
summary: "Inspect or control Provider recovery without stopping an admitted Turn.",
|
|
699
|
+
usage: "yui task role session retry <task> <role> [show|cancel|disable|enable]"
|
|
700
|
+
},
|
|
673
701
|
{
|
|
674
702
|
name: "inspect",
|
|
675
703
|
summary: "Read the current Session, Host process, and AgentRun facts.",
|
|
@@ -1481,7 +1509,7 @@ export const ROOT_COMMAND = buildNode({
|
|
|
1481
1509
|
sections: [
|
|
1482
1510
|
{ id: "global", title: "Global Role sessions", entries: ["context", "enter", "record", "replace"] },
|
|
1483
1511
|
{ id: "maintenance", title: "Maintenance", entries: ["stop"] },
|
|
1484
|
-
{ id: "recovery", title: "Recovery", entries: ["reconcile"] }
|
|
1512
|
+
{ id: "recovery", title: "Recovery", entries: ["reconcile", "retry"] }
|
|
1485
1513
|
],
|
|
1486
1514
|
children: globalSessionChildren
|
|
1487
1515
|
},
|
|
@@ -1489,7 +1517,7 @@ export const ROOT_COMMAND = buildNode({
|
|
|
1489
1517
|
name: "task",
|
|
1490
1518
|
summary: "Manage Tasks, WorkItems, AgentRuns, and integration.",
|
|
1491
1519
|
sections: [
|
|
1492
|
-
{ id: "lifecycle", title: "Lifecycle", entries: ["create", "project", "base", "update", "activate", "activation", "execution", "complete", "cancel", "reopen", "retire", "list", "show", "context", "next-action", "remote-delivery", "archive", "replace", "reconcile", "upstream", "artifact"] },
|
|
1520
|
+
{ id: "lifecycle", title: "Lifecycle", entries: ["create", "project", "base", "update", "activate", "activation", "execution", "complete", "cancel", "reopen", "retire", "list", "show", "context", "next-action", "remote-delivery", "archive-preflight", "archive", "replace", "reconcile", "upstream", "artifact"] },
|
|
1493
1521
|
{ id: "collaboration", title: "Collaboration", entries: ["message", "input", "grant", "workflow", "publication", "work", "run", "review", "integration", "role", "overlap", "change-set"] },
|
|
1494
1522
|
{ id: "knowledge", title: "Task Knowledge", entries: ["brief", "decision", "milestone", "event", "continuation", "wake"] }
|
|
1495
1523
|
],
|
|
@@ -229,6 +229,10 @@ export const INTERACTION_POLICIES = Object.freeze([
|
|
|
229
229
|
...taskTarget("remote-delivery"),
|
|
230
230
|
trailingOptions: { "--json": "flag" }
|
|
231
231
|
},
|
|
232
|
+
{
|
|
233
|
+
...taskTarget("archive-preflight"),
|
|
234
|
+
trailingOptions: { "--integrated": "flag", "--abandon": "flag", "--force": "flag", "--json": "flag" }
|
|
235
|
+
},
|
|
232
236
|
{
|
|
233
237
|
...taskTarget("archive", 2, ["completed", "cancelled"]),
|
|
234
238
|
trailingOptions: { "--integrated": "flag", "--abandon": "flag", "--force": "flag" },
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
export function taskDiagnosticTarget(args) {
|
|
4
4
|
if (args[0] !== "task")
|
|
5
5
|
return undefined;
|
|
6
|
-
if (["show", "context", "next-action"].includes(args[1] ?? "")) {
|
|
6
|
+
if (["show", "context", "next-action", "archive-preflight"].includes(args[1] ?? "")) {
|
|
7
7
|
return args[1] === "context" && ["inspect", "delta"].includes(args[2] ?? "")
|
|
8
8
|
? args[3] : args[2];
|
|
9
9
|
}
|
package/dist/cli.js
CHANGED
|
@@ -42,10 +42,13 @@ import { runResourcesCommand } from "./commands/resourcesCommands.js";
|
|
|
42
42
|
import { applyOperatorSessionControl, runOperatorCommand } from "./commands/operatorCommands.js";
|
|
43
43
|
import { runProjectCommand } from "./commands/projectCommands.js";
|
|
44
44
|
import { previewProfileAgentConfigurationMutation, runProfileCommand } from "./commands/profileCommands.js";
|
|
45
|
-
import { assertWorkItemDependenciesCompletedForCommand, requireWorkItemAssignee, dispatchPreparedReviewRound, failPendingReviewRound, preserveReviewRoundWorkspace, parseTaskCompletionRequest, previewTaskRoleAgentConfigurationMutation, preflightTaskCompletion, runTaskCommand, planReplicatedWorkItemLanes, validateTaskArchiveRequest } from "./commands/taskCommands.js";
|
|
45
|
+
import { assertWorkItemDependenciesCompletedForCommand, requireWorkItemAssignee, dispatchPreparedReviewRound, failPendingReviewRound, preserveReviewRoundWorkspace, parseTaskCompletionRequest, previewTaskRoleAgentConfigurationMutation, preflightTaskCompletion, runTaskCommand, planReplicatedWorkItemLanes, validateTaskArchiveRequest, parseTaskArchiveArguments } from "./commands/taskCommands.js";
|
|
46
46
|
import { assertTaskRemoteDeliveryIntegrated, createTaskRemoteDeliveryProof } from "./commands/taskRemoteDeliveryCommand.js";
|
|
47
47
|
import { renderArchiveDiagnostics, taskArchiveDiagnostics } from "./task/archiveDiagnostics.js";
|
|
48
|
+
import { inspectTaskArchive, renderTaskArchivePreflight } from "./task/archivePreflight.js";
|
|
49
|
+
import { CleanupInspectionError } from "./workspace/cleanupInspection.js";
|
|
48
50
|
import { runTaskPublicationVerifyCommand } from "./commands/taskPublicationVerifyCommand.js";
|
|
51
|
+
import { runTaskPublicationAdoptCommand } from "./commands/taskPublicationAdoptCommand.js";
|
|
49
52
|
import { createGitHubCliPublicationVerifier } from "./external/githubPublicationVerifier.js";
|
|
50
53
|
import { createGitLabCliPublicationVerifier } from "./external/gitlabPublicationVerifier.js";
|
|
51
54
|
import { taskLocalActor, assertTaskDeliveryAuthority } from "./commands/taskActor.js";
|
|
@@ -115,6 +118,9 @@ const rawArgs = [...taskFinalReviewInvocation.args];
|
|
|
115
118
|
const jsonOutput = rawArgs.includes("--json");
|
|
116
119
|
const args = normalizeAliases(jsonOutput ? rawArgs.filter((argument) => argument !== "--json") : rawArgs);
|
|
117
120
|
void main().catch((error) => {
|
|
121
|
+
if (error instanceof CleanupInspectionError) {
|
|
122
|
+
error = cleanupCliError(error, error.checks[0]?.resource ?? "workspace");
|
|
123
|
+
}
|
|
118
124
|
if (error instanceof CliError) {
|
|
119
125
|
const rendered = jsonOutput
|
|
120
126
|
? JSON.stringify({ ok: false, code: error.code, message: error.message, details: error.details })
|
|
@@ -913,6 +919,15 @@ export async function main() {
|
|
|
913
919
|
return;
|
|
914
920
|
}
|
|
915
921
|
if (resolved[0] === "task") {
|
|
922
|
+
if (resolved[1] === "archive-preflight") {
|
|
923
|
+
const request = parseTaskArchiveArguments(resolved.slice(2), "archive-preflight");
|
|
924
|
+
if (process.env.YUI_SESSION_SCOPE === "task" && process.env.YUI_TASK_ID !== request.taskId) {
|
|
925
|
+
throw usageError("Archive inspection must remain within this Session's Task.");
|
|
926
|
+
}
|
|
927
|
+
const data = await inspectTaskArchive(workspaceCoordinator, request);
|
|
928
|
+
emit(renderTaskArchivePreflight(data), false, data);
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
916
931
|
if (resolved[1] === "artifact") {
|
|
917
932
|
// File/directory artifacts live in the Task's local Git repository, so
|
|
918
933
|
// their save/read/list are asynchronous and handled here rather than in
|
|
@@ -1033,6 +1048,13 @@ export async function main() {
|
|
|
1033
1048
|
emit(result.output, false, result.data);
|
|
1034
1049
|
return;
|
|
1035
1050
|
}
|
|
1051
|
+
if (resolved[1] === "publication" && (resolved[2] === "diff" || resolved[2] === "adopt")) {
|
|
1052
|
+
const result = await runTaskPublicationAdoptCommand(resolved.slice(2), store, {
|
|
1053
|
+
environment: process.env
|
|
1054
|
+
});
|
|
1055
|
+
emit(result.output, false, result.data);
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
1036
1058
|
if (resolved[1] === "publication" && resolved[2] === "verify") {
|
|
1037
1059
|
const result = await runTaskPublicationVerifyCommand(resolved.slice(3), store, {
|
|
1038
1060
|
verifiers: {
|
|
@@ -1043,12 +1065,6 @@ export async function main() {
|
|
|
1043
1065
|
environmentPath: process.env.PATH
|
|
1044
1066
|
})
|
|
1045
1067
|
},
|
|
1046
|
-
candidateForTask: async (taskId) => {
|
|
1047
|
-
const status = store.getTask(taskId)?.status;
|
|
1048
|
-
return status === "active" || status === "cancelled"
|
|
1049
|
-
? snapshotActualTaskReviewCandidate(taskId, store, workspacePreparer)
|
|
1050
|
-
: null;
|
|
1051
|
-
},
|
|
1052
1068
|
environment: process.env
|
|
1053
1069
|
});
|
|
1054
1070
|
emit(result.output, false, result.data);
|
|
@@ -1154,7 +1170,7 @@ export async function main() {
|
|
|
1154
1170
|
await new WorkItemChangeSetManager(store).assertIntegrated(reference.taskId, reference.localId);
|
|
1155
1171
|
}
|
|
1156
1172
|
catch (error) {
|
|
1157
|
-
throw
|
|
1173
|
+
throw cleanupCliError(error, `work-item:${qualified}`);
|
|
1158
1174
|
}
|
|
1159
1175
|
}
|
|
1160
1176
|
let removal;
|
|
@@ -1245,15 +1261,15 @@ export async function main() {
|
|
|
1245
1261
|
await new WorkItemChangeSetManager(store).assertIntegrated(task.id, item.id);
|
|
1246
1262
|
}
|
|
1247
1263
|
catch (error) {
|
|
1248
|
-
throw
|
|
1264
|
+
throw cleanupCliError(error, `work-item:${task.id}/${item.id}`);
|
|
1249
1265
|
}
|
|
1250
1266
|
}
|
|
1251
1267
|
const cleanup = await workspaceCoordinator.cleanupTaskForArchive(task.id, disposition);
|
|
1252
1268
|
if (cleanup.status === "retained-dirty") {
|
|
1253
|
-
throw usageError(cleanup.error ?? `Task ${task.id} has dirty managed worktrees and remains terminal.`, undefined, cleanupBlockedDetails(cleanup.reason ?? "dirty-worktree", cleanup.resource ?? `task:${task.id}`, cleanup.retryable ?? true));
|
|
1269
|
+
throw usageError(cleanup.error ?? `Task ${task.id} has dirty managed worktrees and remains terminal.`, undefined, cleanupBlockedDetails(cleanup.reason ?? "dirty-worktree", cleanup.resource ?? `task:${task.id}`, cleanup.retryable ?? true, cleanup.checks));
|
|
1254
1270
|
}
|
|
1255
1271
|
if (cleanup.status === "failed") {
|
|
1256
|
-
throw usageError(`Task ${task.id} worktree cleanup failed: ${cleanup.error ?? "unknown error"}.`, undefined, cleanupBlockedDetails(cleanup.reason ?? "cleanup-failed", cleanup.resource ?? `task:${task.id}`, cleanup.retryable ?? true));
|
|
1272
|
+
throw usageError(`Task ${task.id} worktree cleanup failed: ${cleanup.error ?? "unknown error"}.`, undefined, cleanupBlockedDetails(cleanup.reason ?? "cleanup-failed", cleanup.resource ?? `task:${task.id}`, cleanup.retryable ?? true, cleanup.checks));
|
|
1257
1273
|
}
|
|
1258
1274
|
}
|
|
1259
1275
|
}
|
|
@@ -1953,17 +1969,21 @@ function assertManagedSessionManifest(home, scope) {
|
|
|
1953
1969
|
return manifest;
|
|
1954
1970
|
}
|
|
1955
1971
|
function cleanupCliError(error, fallbackResource) {
|
|
1972
|
+
if (error instanceof CleanupInspectionError) {
|
|
1973
|
+
return usageError(error.message, undefined, cleanupBlockedDetails(error.checks[0]?.reason ?? "cleanup-failed", fallbackResource, true, error.checks));
|
|
1974
|
+
}
|
|
1956
1975
|
if (error instanceof WorkspaceCleanupBlockedError) {
|
|
1957
1976
|
return usageError(error.message, undefined, cleanupBlockedDetails(error.reason, error.resource, error.retryable));
|
|
1958
1977
|
}
|
|
1959
1978
|
return new CliError("RUNTIME_ERROR", error instanceof Error ? error.message : String(error), undefined, cleanupBlockedDetails("cleanup-failed", fallbackResource, true));
|
|
1960
1979
|
}
|
|
1961
|
-
function cleanupBlockedDetails(reason, resource, retryable) {
|
|
1980
|
+
function cleanupBlockedDetails(reason, resource, retryable, checks) {
|
|
1962
1981
|
return {
|
|
1963
1982
|
status: "blocked",
|
|
1964
1983
|
blockedBy: [{ resource, reason, retryable }],
|
|
1965
1984
|
remainingResources: [resource],
|
|
1966
|
-
retryable
|
|
1985
|
+
retryable,
|
|
1986
|
+
...(checks === undefined ? {} : { checks })
|
|
1967
1987
|
};
|
|
1968
1988
|
}
|
|
1969
1989
|
function cliWorkItemReference(value, environment) {
|
|
@@ -2042,9 +2062,20 @@ async function prepareExecutionLaneWorkspacesForCommand(args, store, preparer, e
|
|
|
2042
2062
|
throw usageError(`${item.taskId}/${roleName} already has an active turn.`);
|
|
2043
2063
|
}
|
|
2044
2064
|
}
|
|
2045
|
-
const held = preparer.acquireTaskProjectMaintenanceLocks(item.taskId);
|
|
2065
|
+
const held = await preparer.acquireTaskProjectMaintenanceLocks(item.taskId);
|
|
2046
2066
|
const map = new Map();
|
|
2047
2067
|
try {
|
|
2068
|
+
assertTaskDeliveryAuthority(store, environment, item.taskId);
|
|
2069
|
+
const currentItem = store.getWorkItem(item.taskId, item.id);
|
|
2070
|
+
if (currentItem?.revision !== item.revision
|
|
2071
|
+
|| JSON.stringify(workItemDispatchLanePlan(args, store, currentItem)) !== JSON.stringify(plan)) {
|
|
2072
|
+
throw new Error(`Work item dispatch changed while waiting for Project maintenance: ${item.id}.`);
|
|
2073
|
+
}
|
|
2074
|
+
if (held.current.status !== "active" || held.current.executionGate.state !== "enabled"
|
|
2075
|
+
|| plan.roles.some(roleName => store.getRole(item.taskId, roleName) === null
|
|
2076
|
+
|| store.getActiveRun(item.taskId, roleName) !== null)) {
|
|
2077
|
+
throw new Error(`Task/Role dispatch state changed while waiting for Project maintenance: ${item.taskId}.`);
|
|
2078
|
+
}
|
|
2048
2079
|
const projectPaths = new Map();
|
|
2049
2080
|
for (const { projectId } of held.current.projectBindings) {
|
|
2050
2081
|
const project = store.getProject(projectId);
|
|
@@ -2667,7 +2698,7 @@ function selectionCall(store, catalogs, method, params) {
|
|
|
2667
2698
|
case "role.list": return store.listGlobalRoles();
|
|
2668
2699
|
case "role.show": return store.getGlobalRole(String(params.name ?? ""));
|
|
2669
2700
|
case "project.list": return callOptional(reader, "listProjects");
|
|
2670
|
-
case "task.list": return
|
|
2701
|
+
case "task.list": return store.listTaskChoices();
|
|
2671
2702
|
case "task.integration.list": return store.listIntegrationAttempts(String(params.taskId ?? ""));
|
|
2672
2703
|
case "task.change-set.list": return store.listChangeSets(String(params.taskId ?? ""));
|
|
2673
2704
|
case "task.role.list": return callOptional(reader, "listRoles", [params.taskId]);
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { usageError } from "../errors/cliError.js";
|
|
9
9
|
import { defaultTableWidth, renderTable } from "../output/table.js";
|
|
10
|
+
import { formatUsageMetric } from "../runtime/taskUsageMetrics.js";
|
|
10
11
|
import { runExecutionAudit } from "../observability/executionAudit.js";
|
|
11
12
|
export function parseExecutionAuditOptions(args) {
|
|
12
13
|
const options = {};
|
|
@@ -246,6 +247,15 @@ export function renderExecutionAudit(report, width = defaultTableWidth()) {
|
|
|
246
247
|
else {
|
|
247
248
|
lines.push("", ...sectionError("orchestration", report));
|
|
248
249
|
}
|
|
250
|
+
if (report.usage.status === "ok" && report.usage.data !== undefined) {
|
|
251
|
+
lines.push("", "Observed usage — Task lifetime, observed sources only (audit time window not applied):");
|
|
252
|
+
for (const usage of report.usage.data) {
|
|
253
|
+
lines.push(` ${usage.taskId}: tokens=${formatUsageMetric(usage.tokens)}; tools=${formatUsageMetric(usage.toolCalls)}; elapsed=${formatUsageMetric(usage.elapsedSeconds, "s")}; native execution sum=${formatUsageMetric(usage.executionSeconds, "s")}`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
lines.push("", ...sectionError("usage", report));
|
|
258
|
+
}
|
|
249
259
|
if (report.storage.status === "ok" && report.storage.data !== undefined) {
|
|
250
260
|
const storage = report.storage.data;
|
|
251
261
|
lines.push("", `Storage: backend ${storage.backend} · state.json ${formatBytes(storage.stateJsonBytes)} · yui.db ${formatBytes(storage.databaseBytes)}`
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { roleNotFound, usageError } from "../errors/cliError.js";
|
|
2
|
+
import { controlProviderRetry, providerRetryProjection, renderProviderRetry } from "../runtime/providerRetry.js";
|
|
3
|
+
import { settleGlobalRetryInput } from "../message/globalProviderRetry.js";
|
|
2
4
|
import { createRoleSessionSet, recordRoleAgentSession } from "../executor/agentExecutor.js";
|
|
3
5
|
import { resolveEffectiveLaunch } from "../executor/effectiveLaunch.js";
|
|
4
6
|
import { managedGlobalRoleWorkspace } from "../storage/homeLayout.js";
|
|
@@ -14,9 +16,9 @@ import { hasRoleLaunchContextOptions, validateConfiguredRoleSkills } from "./rol
|
|
|
14
16
|
import { assertLiveRoleSessionAcknowledged, assertRoleRuntimeMutationAllowed, LIVE_SESSION_ACKNOWLEDGEMENT_OPTION } from "./roleRuntimeGuard.js";
|
|
15
17
|
export function runGlobalRoleCommand(args, store, options = {}) {
|
|
16
18
|
const [command, ...rest] = args;
|
|
17
|
-
if (command === "message" || command === "interrupt") {
|
|
19
|
+
if (command === "message" || command === "interrupt" || command === "session" && rest[0] === "retry") {
|
|
18
20
|
const env = options.env ?? process.env;
|
|
19
|
-
const target = command === "message" ? rest[1] : rest[0];
|
|
21
|
+
const target = command === "message" || command === "session" ? rest[1] : rest[0];
|
|
20
22
|
if (env.YUI_SESSION_SCOPE !== undefined && env.YUI_SESSION_SCOPE !== "global"
|
|
21
23
|
|| env.YUI_SESSION_SCOPE === "global" && env.YUI_ROLE !== "operator"
|
|
22
24
|
&& env.YUI_ROLE !== target) {
|
|
@@ -126,6 +128,7 @@ function roleContext(args, store, options) {
|
|
|
126
128
|
pendingMessages: pendingQueue,
|
|
127
129
|
messages: store.listGlobalRoleMessages(name),
|
|
128
130
|
nativeTurn: sessions?.providerBinding?.run ?? null,
|
|
131
|
+
retry: providerRetryProjection(sessions?.providerBinding),
|
|
129
132
|
interrupts: sessions?.interrupts ?? {},
|
|
130
133
|
sessionManifestPath: environment.YUI_SESSION_MANIFEST,
|
|
131
134
|
cliCommand: "yui"
|
|
@@ -382,6 +385,28 @@ function enterRole(args, store) {
|
|
|
382
385
|
}
|
|
383
386
|
function roleSession(args, store, options) {
|
|
384
387
|
const [command, rawName, ...tail] = args;
|
|
388
|
+
if (command === "retry") {
|
|
389
|
+
const name = roleName(rawName);
|
|
390
|
+
const [action = "show", ...extra] = tail;
|
|
391
|
+
if (extra.length > 0 || !["show", "cancel", "disable", "enable"].includes(action)) {
|
|
392
|
+
throw usageError("Usage: yui session retry <role> [show|cancel|disable|enable]");
|
|
393
|
+
}
|
|
394
|
+
return store.transaction(tx => {
|
|
395
|
+
requireRole(name, tx);
|
|
396
|
+
const sessions = tx.getGlobalRoleSessionSet(name);
|
|
397
|
+
let binding = sessions?.providerBinding ?? null;
|
|
398
|
+
if (action !== "show") {
|
|
399
|
+
if (sessions === null || binding === null)
|
|
400
|
+
throw usageError("No current Provider Session.");
|
|
401
|
+
binding = controlProviderRetry(binding, action, Date.now());
|
|
402
|
+
tx.saveGlobalRoleSessionSet({ ...sessions, providerBinding: binding });
|
|
403
|
+
settleGlobalRetryInput(tx, name, binding, new Date());
|
|
404
|
+
}
|
|
405
|
+
return options.jsonOutput ? JSON.stringify({
|
|
406
|
+
roleName: name, retry: providerRetryProjection(binding), disabled: binding?.retryDisabled ?? false
|
|
407
|
+
}) + "\n" : renderProviderRetry(binding) + "\n";
|
|
408
|
+
});
|
|
409
|
+
}
|
|
385
410
|
if (command !== "record" && command !== "replace") {
|
|
386
411
|
throw usageError("Session usage: yui session record|replace <role> --native-id <id> [--reason <reason>].");
|
|
387
412
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { readdir, rename, rm } from "node:fs/promises";
|
|
3
3
|
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
4
|
-
import { usageError } from "../errors/cliError.js";
|
|
4
|
+
import { CliError, usageError } from "../errors/cliError.js";
|
|
5
5
|
import { defaultTableWidth, renderTable } from "../output/table.js";
|
|
6
6
|
import { healCheckoutSwap, restoreCheckoutSwap, swapManagedCheckout } from "../repository/checkoutSwap.js";
|
|
7
|
-
import { NodeGitWorkspace } from "../repository/gitWorkspace.js";
|
|
7
|
+
import { GitWorkspaceRefreshError, NodeGitWorkspace } from "../repository/gitWorkspace.js";
|
|
8
8
|
import { acquireProjectMaintenanceLock } from "../repository/projectMaintenanceLock.js";
|
|
9
9
|
import { addProjectKnowledge, addKnowledgeProposal, assertProjectActive, decideKnowledgeProposal, createProject, findKnowledgeProposal, findKnowledgeProposalByFingerprint, knowledgeEvidenceDigest, knowledgeProposalFingerprint, managedProjectPath, planKnowledgeAcceptance, retireProject, retireProjectKnowledge, resolveProject, updateProjectKnowledge, updateProjectMetadata, validateProject, validateProjectName } from "../repository/project.js";
|
|
10
10
|
import { managedWorkspacesRoot } from "../storage/homeLayout.js";
|
|
@@ -22,10 +22,14 @@ export async function runProjectCommand(args, store, options = {}) {
|
|
|
22
22
|
}
|
|
23
23
|
if (command === "refresh") {
|
|
24
24
|
const refreshed = await refreshProject(rest, store, options);
|
|
25
|
+
const head = refreshed.changed
|
|
26
|
+
? `Refreshed project ${refreshed.project.id}: ${refreshed.fromCommit} -> ${refreshed.toCommit}`
|
|
27
|
+
: `Project ${refreshed.project.id} HEAD is already current at ${refreshed.toCommit}`;
|
|
28
|
+
const tracking = refreshed.tracking;
|
|
25
29
|
return {
|
|
26
|
-
output:
|
|
27
|
-
? `
|
|
28
|
-
: `
|
|
30
|
+
output: `${head}\n` + (tracking.status === "unmanaged"
|
|
31
|
+
? `Tracking unmanaged: ${tracking.reason}\n`
|
|
32
|
+
: `Tracking ${tracking.status}: ${tracking.ref} at ${tracking.toCommit}\n`),
|
|
29
33
|
data: refreshed
|
|
30
34
|
};
|
|
31
35
|
}
|
|
@@ -101,14 +105,23 @@ async function refreshProject(args, store, options) {
|
|
|
101
105
|
// RFC Phase 1: hold the per-Project maintenance fence for the whole refresh
|
|
102
106
|
// so Task workspace preparation and other maintenance cannot interleave
|
|
103
107
|
// with the canonical branch/working-tree move.
|
|
104
|
-
const releaseMaintenance = acquireProjectMaintenanceLock(store.rootDirectory(), project.id);
|
|
108
|
+
const releaseMaintenance = await acquireProjectMaintenanceLock(store.rootDirectory(), project.id, { signal: options.signal });
|
|
105
109
|
try {
|
|
110
|
+
const current = requireUnchangedMaintenanceProject(store, project, "refresh");
|
|
106
111
|
const refreshed = await (options.git ?? new NodeGitWorkspace()).refresh({
|
|
107
|
-
repositoryPath:
|
|
108
|
-
remoteUrl:
|
|
109
|
-
stableRef:
|
|
112
|
+
repositoryPath: current.path,
|
|
113
|
+
remoteUrl: current.remoteUrl,
|
|
114
|
+
stableRef: current.stableBranch
|
|
110
115
|
});
|
|
111
|
-
return { project, ...refreshed };
|
|
116
|
+
return { project: current, ...refreshed };
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
if (error instanceof GitWorkspaceRefreshError) {
|
|
120
|
+
throw new CliError("RUNTIME_ERROR", error.message, undefined, {
|
|
121
|
+
projectId: project.id, refresh: error.result
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
throw error;
|
|
112
125
|
}
|
|
113
126
|
finally {
|
|
114
127
|
releaseMaintenance();
|
|
@@ -131,8 +144,9 @@ async function diagnoseProject(args, store, options) {
|
|
|
131
144
|
};
|
|
132
145
|
}
|
|
133
146
|
const git = options.git ?? new NodeGitWorkspace();
|
|
134
|
-
const releaseMaintenance = acquireProjectMaintenanceLock(store.rootDirectory(), project.id);
|
|
147
|
+
const releaseMaintenance = await acquireProjectMaintenanceLock(store.rootDirectory(), project.id, { signal: options.signal });
|
|
135
148
|
try {
|
|
149
|
+
requireUnchangedMaintenanceProject(store, project, "diagnose");
|
|
136
150
|
const current = await git.inspect(project.path, "HEAD");
|
|
137
151
|
const remote = await git.resolveRemoteBaseline({
|
|
138
152
|
repositoryPath: project.path,
|
|
@@ -307,13 +321,14 @@ async function migrateProject(args, store, options) {
|
|
|
307
321
|
// Migration rewrites the Project's Git repository: hold the per-Project
|
|
308
322
|
// maintenance fence so no rebuild/archive/cleanup (or a second migrate)
|
|
309
323
|
// interleaves, and the Controller defers worktree preparation meanwhile.
|
|
310
|
-
const releaseMaintenance = acquireProjectMaintenanceLock(store.rootDirectory(), project.id);
|
|
324
|
+
const releaseMaintenance = await acquireProjectMaintenanceLock(store.rootDirectory(), project.id, { signal: options.signal });
|
|
311
325
|
try {
|
|
312
326
|
// Re-read the Project under the fence. A concurrent migration may have
|
|
313
327
|
// completed and switched the catalog to the Home-managed repo since the
|
|
314
328
|
// record was resolved above; the stale snapshot must not drive any Git
|
|
315
329
|
// effect (in particular it must not delete the now-canonical repo).
|
|
316
330
|
const current = requireProject(store, project.id);
|
|
331
|
+
assertProjectActive(current, "migrate");
|
|
317
332
|
if (current.ownership === "managed") {
|
|
318
333
|
throw usageError(`Project is already Home-managed: ${project.id}.`);
|
|
319
334
|
}
|
|
@@ -588,7 +603,7 @@ async function resetProject(args, store, options) {
|
|
|
588
603
|
throw usageError(`Project reset requires matching stable and development branches: ${project.id}.`);
|
|
589
604
|
}
|
|
590
605
|
const git = options.git ?? new NodeGitWorkspace();
|
|
591
|
-
const releaseMaintenance = acquireProjectMaintenanceLock(store.rootDirectory(), project.id);
|
|
606
|
+
const releaseMaintenance = await acquireProjectMaintenanceLock(store.rootDirectory(), project.id, { signal: options.signal });
|
|
592
607
|
try {
|
|
593
608
|
// Re-read under the fence so a concurrent catalog change can never drive
|
|
594
609
|
// a destructive Git effect from a stale snapshot.
|
|
@@ -713,7 +728,7 @@ async function replaceProject(args, store, options) {
|
|
|
713
728
|
+ "Re-run with --discard-local to acknowledge that the checkout and any uncommitted state will be discarded.");
|
|
714
729
|
}
|
|
715
730
|
const git = options.git ?? new NodeGitWorkspace();
|
|
716
|
-
const releaseMaintenance = acquireProjectMaintenanceLock(store.rootDirectory(), project.id);
|
|
731
|
+
const releaseMaintenance = await acquireProjectMaintenanceLock(store.rootDirectory(), project.id, { signal: options.signal });
|
|
717
732
|
try {
|
|
718
733
|
const current = requireProject(store, project.id);
|
|
719
734
|
assertProjectActive(current, "replace");
|
|
@@ -922,8 +937,9 @@ async function deleteProjectCommand(args, store, options) {
|
|
|
922
937
|
// every failure restores it, so the catalog never loses its recoverable
|
|
923
938
|
// entry while a live checkout (or its failure) is still in play.
|
|
924
939
|
const tombstone = join(store.rootDirectory(), "projects", `.delete-${project.id}`);
|
|
925
|
-
const releaseMaintenance = acquireProjectMaintenanceLock(store.rootDirectory(), project.id);
|
|
940
|
+
const releaseMaintenance = await acquireProjectMaintenanceLock(store.rootDirectory(), project.id, { signal: options.signal });
|
|
926
941
|
try {
|
|
942
|
+
requireUnchangedMaintenanceProject(store, project, "delete");
|
|
927
943
|
// Heal a crashed earlier attempt before the prechecks turn.
|
|
928
944
|
await healCheckoutSwap({ currentPath: project.path, backupPath: tombstone });
|
|
929
945
|
if (existsSync(project.path)) {
|
|
@@ -1289,6 +1305,19 @@ function projectKnowledge(args, store, options) {
|
|
|
1289
1305
|
? "Project knowledge command is required."
|
|
1290
1306
|
: `Unknown command: project knowledge ${command}`);
|
|
1291
1307
|
}
|
|
1308
|
+
/** Waiting yields to catalog writers too; never apply stale Git/lifecycle inputs. */
|
|
1309
|
+
function requireUnchangedMaintenanceProject(store, expected, action) {
|
|
1310
|
+
const current = requireProject(store, expected.id);
|
|
1311
|
+
if (current.path !== expected.path
|
|
1312
|
+
|| current.remoteUrl !== expected.remoteUrl
|
|
1313
|
+
|| current.stableBranch !== expected.stableBranch
|
|
1314
|
+
|| current.developmentBranch !== expected.developmentBranch
|
|
1315
|
+
|| current.ownership !== expected.ownership
|
|
1316
|
+
|| current.status !== expected.status) {
|
|
1317
|
+
throw new Error(`Project changed while waiting to ${action}: ${expected.id}; read current state before retrying.`);
|
|
1318
|
+
}
|
|
1319
|
+
return current;
|
|
1320
|
+
}
|
|
1292
1321
|
function requireProject(store, reference) {
|
|
1293
1322
|
const project = resolveProject(store.listProjects(), reference);
|
|
1294
1323
|
if (project === null)
|