@tea-agent/loop-agent 0.33.5 → 0.33.6
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/CHANGELOG.md +33 -0
- package/dist/application/task-lifecycle/advance.js +254 -4
- package/dist/application/task-lifecycle/gates.js +50 -0
- package/dist/application/task-lifecycle/observe.js +11 -2
- package/dist/commands/init-upgrade.js +32 -1
- package/dist/commands/init.js +94 -3
- package/dist/executors/shell-write-guard.js +26 -8
- package/dist/shared/operator/capabilities.js +72 -42
- package/dist/shared/resilient-git.js +133 -0
- package/dist/task/source-prepare/artifact-meta.js +137 -0
- package/dist/task/source-prepare/index.js +2 -0
- package/dist/task/source-prepare/parse-intent.js +58 -10
- package/dist/task/source-prepare/prepare.js +180 -16
- package/dist/task/source-prepare/reference-integrity.js +18 -2
- package/dist/task/source-prepare/semantic-intake.js +404 -0
- package/dist/worker/console/app-data.js +2 -0
- package/dist/worker/console/chat/chat-event-store.js +190 -25
- package/dist/worker/console/chat/pi-console-config.js +250 -32
- package/dist/worker/console/chat/pi-runtime.js +625 -71
- package/dist/worker/console/chat/resource-loader.js +5 -4
- package/dist/worker/console/chat/routes.js +324 -146
- package/dist/worker/console/chat/runtime-context.js +48 -12
- package/dist/worker/console/chat/runtime-selection.js +59 -0
- package/dist/worker/console/chat/shortcuts.js +1 -0
- package/dist/worker/console/chat/tool-adapter.js +9 -3
- package/dist/worker/console/chat/tools.js +5 -1
- package/dist/worker/console/dag-execution-receipt.js +380 -0
- package/dist/worker/console/operator-actions.js +559 -68
- package/dist/worker/console/server.js +8 -15
- package/dist/worker/console/static/assets/index-BUOLppPr.js +28 -0
- package/dist/worker/console/static/assets/index-C1KzazY5.css +1 -0
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +45 -8
- package/dist/worker/console/static-src/operator-chat/refs.js +9 -0
- package/dist/worker/console/static-src/operator-chat/runtime-snapshot-store.js +257 -0
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +16 -0
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +210 -184
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +49 -5
- package/dist/worker/console/static-src/operator-chat/useComposer.js +17 -0
- package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +225 -74
- package/dist/worker/console/static-src/operator-chat/useRuntimeSnapshot.js +196 -0
- package/dist/worker/delivery/final-verification.js +13 -5
- package/dist/worker/delivery/package.js +31 -19
- package/dist/worker/delivery/verification-bundle.js +6 -4
- package/dist/worker/observe/static/operator-chrome.css +5 -2
- package/dist/worker/observe/static/operator-chrome.js +6 -1
- package/dist/worker/observe/static/styles.css +39 -9
- package/dist/workflows/dag/frontend-worktree-diff.js +12 -27
- package/dist/workflows/dag/workspace-checkpoint.js +8 -27
- package/harness.json +1 -1
- package/package.json +1 -1
- package/skills/loop-agent/references/command-reference.md +3 -1
- package/skills/loop-agent/references/source-and-plan-practice.md +13 -0
- package/skills/loop-agent/references/task-workflow.md +4 -0
- package/dist/worker/console/chat/instruction-skills.js +0 -217
- package/dist/worker/console/static/assets/index-CnUXAqxG.css +0 -1
- package/dist/worker/console/static/assets/index-CteJFFL2.js +0 -29
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,39 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.33.6] - 2026-08-11
|
|
6
|
+
|
|
7
|
+
### 新增
|
|
8
|
+
|
|
9
|
+
- stabilize PRD intake + add Semantic Intake fallback
|
|
10
|
+
- Operator Chat 单事件流 + boot epoch fence(Task C)
|
|
11
|
+
- gate writeSet delta + run summary(操作面增强)
|
|
12
|
+
- supervise DAG runs autonomously
|
|
13
|
+
- ChatEventStore 事件持久化压缩(coalescing + maxBytes 双限制)
|
|
14
|
+
- reconcile(cursor_expired) 自动恢复而非全局致命错误
|
|
15
|
+
- 接入 Operator Chat 实时运行时控制
|
|
16
|
+
- open operator chat resource loading
|
|
17
|
+
- enrich execution report and URL resolution
|
|
18
|
+
|
|
19
|
+
### 改进
|
|
20
|
+
|
|
21
|
+
- style(task-advance): biome autoformat 对齐(无功能变化)
|
|
22
|
+
- style(console): apply biome formatting from pi-lens autoformat
|
|
23
|
+
|
|
24
|
+
### 修复
|
|
25
|
+
|
|
26
|
+
- unify resilient read-only command execution
|
|
27
|
+
- preserve user-owned Pi model routing
|
|
28
|
+
- show installed package metadata
|
|
29
|
+
- escape preflight backtick matcher
|
|
30
|
+
- align chat streaming reliability with pi-web
|
|
31
|
+
- split rerun defaults and repair selector escaping
|
|
32
|
+
- ignore delimiters inside pytest strings
|
|
33
|
+
- resolve target URL from runtime context
|
|
34
|
+
- reject priority-only module stems
|
|
35
|
+
- stop desiredSha256 mechanical checks from blocking generated-content upgrades
|
|
36
|
+
- use mobius header brand icon
|
|
37
|
+
|
|
5
38
|
## [0.33.5] - 2026-08-11
|
|
6
39
|
|
|
7
40
|
### 重点更新
|
|
@@ -3,6 +3,9 @@ import path from "node:path";
|
|
|
3
3
|
import { createTask, getTaskPaths, loadTaskConfig, } from "../../task/runtime.js";
|
|
4
4
|
import { importPrdDocument } from "../../task/source-references.js";
|
|
5
5
|
import { prepareTaskSource } from "../../task/source-prepare/index.js";
|
|
6
|
+
import { isIntakeSoftGapCode, resolvePrdImportRole, } from "../../task/source-prepare/artifact-meta.js";
|
|
7
|
+
import { evaluateSemanticIntakeEligibility, isSemanticIntakeDisabled, readSemanticIntakeAttempted, runSemanticIntake, } from "../../task/source-prepare/semantic-intake.js";
|
|
8
|
+
import { validateImportedPrdReferences } from "../../task/source-prepare/reference-integrity.js";
|
|
6
9
|
import { logicalRevisionForState, observeTaskContractState, recoverTaskContract, } from "../../task/contract/index.js";
|
|
7
10
|
import { generateTaskDagUseCase } from "../dag/generate-task-dag.js";
|
|
8
11
|
import { validateDagUseCase } from "../dag/validate-dag.js";
|
|
@@ -10,7 +13,7 @@ import { runDagUseCase } from "../dag/run-dag.js";
|
|
|
10
13
|
import { promoteRunEvidence, closeoutTask } from "../../records/promotion.js";
|
|
11
14
|
import { resolveRunningControllerIdentity } from "../../shared/package-metadata.js";
|
|
12
15
|
import { assessDagRunLiveness, locateDagRun, readDagRunState, } from "../../workflows/dag/lifecycle.js";
|
|
13
|
-
import { buildGateBinding, buildWriteSetGate, controllerIdentityAnchor, gateMatches, hashCanonicalPayload, parseGateToken, } from "./gates.js";
|
|
16
|
+
import { buildGateBinding, buildWriteSetGate, buildWriteSetGateDelta, controllerIdentityAnchor, gateMatches, hashCanonicalPayload, parseGateToken, } from "./gates.js";
|
|
14
17
|
import { appendTransition, emptyLifecycleRecord, loadLifecycleRecord, recordRunAssociation, saveLifecycleRecord, } from "./record.js";
|
|
15
18
|
import { collectNormalizedWriteSetFromSpec, hashDagSpecFileContent, observeTaskLifecycle, } from "./observe.js";
|
|
16
19
|
import { planTransitions, } from "./plan-transitions.js";
|
|
@@ -98,6 +101,42 @@ function toRunAssociation(input) {
|
|
|
98
101
|
source: input.source ?? "lifecycle-record",
|
|
99
102
|
};
|
|
100
103
|
}
|
|
104
|
+
/** Compact run completion summary for the advance result (AC-005/AC-006):
|
|
105
|
+
* per-node status, first VERDICT line found in any node stdout, and nodes
|
|
106
|
+
* flagged with a write-guard / out-of-bounds failure category. */
|
|
107
|
+
function buildRunSummary(state) {
|
|
108
|
+
const nodes = Object.entries(state.nodes ?? {}).map(([id, node]) => ({
|
|
109
|
+
id,
|
|
110
|
+
status: node.status,
|
|
111
|
+
}));
|
|
112
|
+
const ordered = (state.ranks ?? []).flat();
|
|
113
|
+
nodes.sort((a, b) => {
|
|
114
|
+
const ai = ordered.indexOf(a.id);
|
|
115
|
+
const bi = ordered.indexOf(b.id);
|
|
116
|
+
return ((ai === -1 ? ordered.length : ai) - (bi === -1 ? ordered.length : bi));
|
|
117
|
+
});
|
|
118
|
+
let verdict = null;
|
|
119
|
+
const outOfBounds = [];
|
|
120
|
+
for (const [id, node] of Object.entries(state.nodes ?? {})) {
|
|
121
|
+
if (!verdict && typeof node.stdout === "string") {
|
|
122
|
+
const match = node.stdout.match(/^\s*VERDICT:\s*(.+)$/im);
|
|
123
|
+
if (match)
|
|
124
|
+
verdict = match[1].trim();
|
|
125
|
+
}
|
|
126
|
+
if (node.failureCategory &&
|
|
127
|
+
/^write-guard|out-of-bounds|write-set|boundary/i.test(node.failureCategory)) {
|
|
128
|
+
outOfBounds.push({ nodeId: id, failureCategory: node.failureCategory });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
runId: state.runId,
|
|
133
|
+
status: state.status,
|
|
134
|
+
nodes,
|
|
135
|
+
...(verdict ? { verdict } : { verdict: null }),
|
|
136
|
+
...(outOfBounds.length > 0 ? { outOfBounds } : {}),
|
|
137
|
+
failureCategory: state.failureCategory ?? null,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
101
140
|
async function buildCurrentGate(repoRoot, taskId) {
|
|
102
141
|
const paths = getTaskPaths(repoRoot, taskId);
|
|
103
142
|
if (!(await pathExists(paths.dagDraftPath)))
|
|
@@ -113,6 +152,7 @@ async function buildCurrentGate(repoRoot, taskId) {
|
|
|
113
152
|
const contractState = await observeTaskContractState(repoRoot, taskId);
|
|
114
153
|
const identity = resolveRunningControllerIdentity();
|
|
115
154
|
let writeSet = collectNormalizedWriteSetFromSpec(spec);
|
|
155
|
+
let contractAllowedPaths = [];
|
|
116
156
|
if (writeSet.length === 0) {
|
|
117
157
|
try {
|
|
118
158
|
const config = await loadTaskConfig(repoRoot, taskId);
|
|
@@ -122,6 +162,18 @@ async function buildCurrentGate(repoRoot, taskId) {
|
|
|
122
162
|
// leave empty
|
|
123
163
|
}
|
|
124
164
|
}
|
|
165
|
+
else {
|
|
166
|
+
try {
|
|
167
|
+
const config = await loadTaskConfig(repoRoot, taskId);
|
|
168
|
+
contractAllowedPaths = config.allowedPaths ?? [];
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
// leave empty
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
const writers = spec.tasks
|
|
175
|
+
.filter((task) => task.writePolicy === "exclusive")
|
|
176
|
+
.map((task) => ({ id: task.id, writeSet: task.writeSet ?? [] }));
|
|
125
177
|
const bound = buildGateBinding({
|
|
126
178
|
taskId,
|
|
127
179
|
contractRevision: logicalRevisionForState(contractState),
|
|
@@ -141,7 +193,12 @@ async function buildCurrentGate(repoRoot, taskId) {
|
|
|
141
193
|
}
|
|
142
194
|
: null),
|
|
143
195
|
});
|
|
144
|
-
|
|
196
|
+
const delta = buildWriteSetGateDelta({
|
|
197
|
+
writeSet,
|
|
198
|
+
contractAllowedPaths,
|
|
199
|
+
writers,
|
|
200
|
+
});
|
|
201
|
+
return buildWriteSetGate({ taskId, bound, delta });
|
|
145
202
|
}
|
|
146
203
|
async function monitorRun(input) {
|
|
147
204
|
const started = Date.now();
|
|
@@ -213,6 +270,7 @@ export async function advanceTaskLifecycle(input) {
|
|
|
213
270
|
let promotion = null;
|
|
214
271
|
let closeout = null;
|
|
215
272
|
let run = null;
|
|
273
|
+
let runSummary = null;
|
|
216
274
|
let consumeApproveGate = Boolean(input.approveGateToken);
|
|
217
275
|
/** After a successful approve, keep approve intent for finalize replan only. */
|
|
218
276
|
let approvedThisCall = false;
|
|
@@ -226,6 +284,7 @@ export async function advanceTaskLifecycle(input) {
|
|
|
226
284
|
let preparedContractThisCall = false;
|
|
227
285
|
/** A successful managed-contract mutation invalidates the prior DAG/gate. */
|
|
228
286
|
let regenerateDagThisCall = false;
|
|
287
|
+
let semanticIntakeAttemptedThisCall = false;
|
|
229
288
|
let snapshot = await observeTaskLifecycle(input.repoRoot, input.taskId);
|
|
230
289
|
let record = (await loadLifecycleRecord(input.repoRoot, input.taskId)) ??
|
|
231
290
|
emptyLifecycleRecord(input.taskId);
|
|
@@ -457,11 +516,25 @@ export async function advanceTaskLifecycle(input) {
|
|
|
457
516
|
}
|
|
458
517
|
if (action.type === "import-prd") {
|
|
459
518
|
for (const prd of input.prdPaths ?? []) {
|
|
519
|
+
let markdown;
|
|
520
|
+
try {
|
|
521
|
+
markdown = await readFile(path.resolve(prd.filePath), "utf-8");
|
|
522
|
+
}
|
|
523
|
+
catch {
|
|
524
|
+
markdown = undefined;
|
|
525
|
+
}
|
|
526
|
+
const role = resolvePrdImportRole({
|
|
527
|
+
filePath: prd.filePath,
|
|
528
|
+
explicitRole: prd.role,
|
|
529
|
+
markdown,
|
|
530
|
+
});
|
|
460
531
|
await importPrdDocument({
|
|
461
532
|
repoRoot: input.repoRoot,
|
|
462
533
|
taskId: input.taskId,
|
|
463
534
|
filePath: prd.filePath,
|
|
464
|
-
role
|
|
535
|
+
role,
|
|
536
|
+
// Prefer basename-aligned names for multi-file product suites.
|
|
537
|
+
name: path.basename(prd.filePath).replace(/\.(md|markdown|txt)$/iu, "") || undefined,
|
|
465
538
|
});
|
|
466
539
|
}
|
|
467
540
|
// Latch even when prdPaths is empty so a mis-planned import cannot loop.
|
|
@@ -494,7 +567,7 @@ export async function advanceTaskLifecycle(input) {
|
|
|
494
567
|
break;
|
|
495
568
|
}
|
|
496
569
|
}
|
|
497
|
-
|
|
570
|
+
let prepareResult = await prepareTaskSource({
|
|
498
571
|
repoRoot: input.repoRoot,
|
|
499
572
|
taskId: input.taskId,
|
|
500
573
|
mode: "apply",
|
|
@@ -520,12 +593,153 @@ export async function advanceTaskLifecycle(input) {
|
|
|
520
593
|
},
|
|
521
594
|
});
|
|
522
595
|
if (!prepareResult.ok) {
|
|
596
|
+
const gapCodes = (prepareResult.gaps ?? [])
|
|
597
|
+
.filter((gap) => gap.level === "blocking")
|
|
598
|
+
.map((gap) => gap.code);
|
|
599
|
+
const softIntake = Boolean(prepareResult.error?.details?.softIntake) ||
|
|
600
|
+
(gapCodes.length > 0 &&
|
|
601
|
+
gapCodes.every((code) => isIntakeSoftGapCode(code)));
|
|
602
|
+
// P2: one-shot semantic intake when structure failed but sources exist.
|
|
603
|
+
if (softIntake &&
|
|
604
|
+
(completed.includes("prd-imported") ||
|
|
605
|
+
(snapshot.source?.importedPrdCount ?? 0) > 0) &&
|
|
606
|
+
!semanticIntakeAttemptedThisCall) {
|
|
607
|
+
const already = await readSemanticIntakeAttempted(input.repoRoot, input.taskId);
|
|
608
|
+
const eligibility = evaluateSemanticIntakeEligibility({
|
|
609
|
+
hasImportedPrd: true,
|
|
610
|
+
gaps: prepareResult.gaps ?? [],
|
|
611
|
+
alreadyAttempted: already,
|
|
612
|
+
disabled: isSemanticIntakeDisabled(),
|
|
613
|
+
});
|
|
614
|
+
if (eligibility.eligible) {
|
|
615
|
+
semanticIntakeAttemptedThisCall = true;
|
|
616
|
+
warnings.push({
|
|
617
|
+
code: "SEMANTIC_INTAKE_STARTING",
|
|
618
|
+
message: eligibility.reason,
|
|
619
|
+
});
|
|
620
|
+
const integrity = await validateImportedPrdReferences({
|
|
621
|
+
repoRoot: input.repoRoot,
|
|
622
|
+
taskId: input.taskId,
|
|
623
|
+
requireParseableRequirement: false,
|
|
624
|
+
});
|
|
625
|
+
const documents = integrity.documents
|
|
626
|
+
.filter((doc) => doc.content)
|
|
627
|
+
.map((doc) => ({
|
|
628
|
+
role: doc.role,
|
|
629
|
+
materializedPath: doc.materializedPath,
|
|
630
|
+
absolutePath: doc.absolutePath,
|
|
631
|
+
content: doc.content,
|
|
632
|
+
}));
|
|
633
|
+
const semantic = await runSemanticIntake({
|
|
634
|
+
repoRoot: input.repoRoot,
|
|
635
|
+
taskId: input.taskId,
|
|
636
|
+
title: input.title ?? prepareResult.title ?? input.taskId,
|
|
637
|
+
taskKind: input.taskKind ?? prepareResult.taskKind,
|
|
638
|
+
featureId: input.featureId,
|
|
639
|
+
documents,
|
|
640
|
+
flags: {
|
|
641
|
+
title: input.title,
|
|
642
|
+
taskKind: input.taskKind,
|
|
643
|
+
featureId: input.featureId,
|
|
644
|
+
allowedPaths: effectiveInput.allowedPaths,
|
|
645
|
+
forbiddenPaths: input.forbiddenPaths,
|
|
646
|
+
verifyCommands: effectiveInput.verifyCommands,
|
|
647
|
+
},
|
|
648
|
+
});
|
|
649
|
+
if (semantic.ok) {
|
|
650
|
+
const retry = await prepareTaskSource({
|
|
651
|
+
repoRoot: input.repoRoot,
|
|
652
|
+
taskId: input.taskId,
|
|
653
|
+
mode: "apply",
|
|
654
|
+
intent: {
|
|
655
|
+
kind: "draft",
|
|
656
|
+
draft: semantic.draft,
|
|
657
|
+
sourcePath: semantic.artifactPath,
|
|
658
|
+
},
|
|
659
|
+
flags: {
|
|
660
|
+
title: input.title,
|
|
661
|
+
taskKind: input.taskKind,
|
|
662
|
+
featureId: input.featureId,
|
|
663
|
+
allowedPaths: effectiveInput.allowedPaths,
|
|
664
|
+
forbiddenPaths: input.forbiddenPaths,
|
|
665
|
+
verifyCommands: effectiveInput.verifyCommands,
|
|
666
|
+
},
|
|
667
|
+
});
|
|
668
|
+
if (retry.ok) {
|
|
669
|
+
warnings.push({
|
|
670
|
+
code: "SEMANTIC_INTAKE_APPLIED",
|
|
671
|
+
message: "structured requirement draft via semantic intake; projected managed source",
|
|
672
|
+
});
|
|
673
|
+
preparedContractThisCall = true;
|
|
674
|
+
if (refreshManagedContractThisCall || input.fromDraftPath) {
|
|
675
|
+
refreshManagedContractThisCall = false;
|
|
676
|
+
regenerateDagThisCall = true;
|
|
677
|
+
}
|
|
678
|
+
record = appendTransition(record, "contract-managed");
|
|
679
|
+
completed.push("contract-managed");
|
|
680
|
+
await saveLifecycleRecord(input.repoRoot, record);
|
|
681
|
+
snapshot = await observeTaskLifecycle(input.repoRoot, input.taskId);
|
|
682
|
+
continue;
|
|
683
|
+
}
|
|
684
|
+
prepareResult = retry;
|
|
685
|
+
// fall through with retry gaps
|
|
686
|
+
}
|
|
687
|
+
else {
|
|
688
|
+
warnings.push({
|
|
689
|
+
code: semantic.code,
|
|
690
|
+
message: semantic.message,
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
else if (!already) {
|
|
695
|
+
warnings.push({
|
|
696
|
+
code: "SEMANTIC_INTAKE_SKIPPED",
|
|
697
|
+
message: eligibility.reason,
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
// Soft intake: PRD may already be imported; stop before DAG without
|
|
702
|
+
// treating the whole advance as a hard parse failure.
|
|
703
|
+
const gapCodesAfter = (prepareResult.gaps ?? [])
|
|
704
|
+
.filter((gap) => gap.level === "blocking")
|
|
705
|
+
.map((gap) => gap.code);
|
|
706
|
+
const softIntakeAfter = Boolean(prepareResult.error?.details?.softIntake) ||
|
|
707
|
+
(gapCodesAfter.length > 0 &&
|
|
708
|
+
gapCodesAfter.every((code) => isIntakeSoftGapCode(code)));
|
|
709
|
+
if (softIntakeAfter && completed.includes("prd-imported")) {
|
|
710
|
+
warnings.push({
|
|
711
|
+
code: "INTAKE_INCOMPLETE",
|
|
712
|
+
message: prepareResult.error?.message ??
|
|
713
|
+
"PRD imported but intake incomplete; DAG not generated",
|
|
714
|
+
});
|
|
715
|
+
for (const gap of prepareResult.gaps ?? []) {
|
|
716
|
+
if (gap.level === "blocking") {
|
|
717
|
+
warnings.push({ code: gap.code, message: gap.message });
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
snapshot = await observeTaskLifecycle(input.repoRoot, input.taskId);
|
|
721
|
+
return {
|
|
722
|
+
taskId: input.taskId,
|
|
723
|
+
lifecycleState: "intake-incomplete",
|
|
724
|
+
completedTransitions: uniqueTransitions(record.completedTransitions, completed),
|
|
725
|
+
gate: null,
|
|
726
|
+
blockers: [],
|
|
727
|
+
warnings,
|
|
728
|
+
next: {
|
|
729
|
+
kind: "advance",
|
|
730
|
+
command: prepareResult.next?.[0] ??
|
|
731
|
+
`loop-agent task advance ${input.taskId} --prd <product-requirement.md> --allowed-path <glob> --json`,
|
|
732
|
+
},
|
|
733
|
+
recommendations,
|
|
734
|
+
};
|
|
735
|
+
}
|
|
523
736
|
blockers.push({
|
|
524
737
|
code: prepareResult.error?.code ?? "PREPARE_FAILED",
|
|
525
738
|
message: prepareResult.error?.message ?? "task source prepare failed",
|
|
526
739
|
details: {
|
|
527
740
|
gaps: prepareResult.gaps,
|
|
528
741
|
outcome: prepareResult.outcome,
|
|
742
|
+
softIntake: softIntakeAfter,
|
|
529
743
|
},
|
|
530
744
|
});
|
|
531
745
|
break;
|
|
@@ -814,6 +1028,22 @@ export async function advanceTaskLifecycle(input) {
|
|
|
814
1028
|
}
|
|
815
1029
|
await saveLifecycleRecord(input.repoRoot, record);
|
|
816
1030
|
}
|
|
1031
|
+
// Build the compact run summary (AC-005/AC-006) from the settled run
|
|
1032
|
+
// state so the operator sees node statuses/verdict/outOfBounds without
|
|
1033
|
+
// reading state.json. Best-effort: skip when the run dir is unavailable.
|
|
1034
|
+
if (run?.runDir) {
|
|
1035
|
+
try {
|
|
1036
|
+
const settledState = await readDagRunState(run.runDir);
|
|
1037
|
+
const summary = buildRunSummary(settledState);
|
|
1038
|
+
if (summary) {
|
|
1039
|
+
summary.next = snapshot.next;
|
|
1040
|
+
runSummary = summary;
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
catch {
|
|
1044
|
+
// leave runSummary null
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
817
1047
|
snapshot = await observeTaskLifecycle(input.repoRoot, input.taskId);
|
|
818
1048
|
// Re-plan so promote/closeout can run in the same call after success.
|
|
819
1049
|
continue;
|
|
@@ -869,6 +1099,25 @@ export async function advanceTaskLifecycle(input) {
|
|
|
869
1099
|
(await buildCurrentGate(input.repoRoot, input.taskId)))
|
|
870
1100
|
: snapshot.gate;
|
|
871
1101
|
await exportDagDraftIfRequested(input.repoRoot, input.taskId, input.dagOutputPath);
|
|
1102
|
+
// Fall back to the latest associated run state when this call did not
|
|
1103
|
+
// itself monitor/start a run (e.g. re-observe after a previously settled
|
|
1104
|
+
// run): the operator still wants the compact summary in the result.
|
|
1105
|
+
if (!runSummary) {
|
|
1106
|
+
const settledRun = run ?? snapshot.latestRun ?? snapshot.activeRun;
|
|
1107
|
+
if (settledRun?.runDir) {
|
|
1108
|
+
try {
|
|
1109
|
+
const settledState = await readDagRunState(settledRun.runDir);
|
|
1110
|
+
const summary = buildRunSummary(settledState);
|
|
1111
|
+
if (summary) {
|
|
1112
|
+
summary.next = snapshot.next;
|
|
1113
|
+
runSummary = summary;
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
catch {
|
|
1117
|
+
// leave runSummary null
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
872
1121
|
return {
|
|
873
1122
|
taskId: input.taskId,
|
|
874
1123
|
lifecycleState: snapshot.lifecycleState,
|
|
@@ -878,6 +1127,7 @@ export async function advanceTaskLifecycle(input) {
|
|
|
878
1127
|
warnings,
|
|
879
1128
|
next: snapshot.next,
|
|
880
1129
|
run,
|
|
1130
|
+
runSummary,
|
|
881
1131
|
promotion,
|
|
882
1132
|
closeout,
|
|
883
1133
|
recommendations,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import { pathMatchesPattern } from "../../shared/git-progress.js";
|
|
2
3
|
export function normalizeWriteSet(writeSet) {
|
|
3
4
|
return [
|
|
4
5
|
...new Set(writeSet.map((entry) => entry.replace(/\\/g, "/").trim()).filter(Boolean)),
|
|
@@ -42,6 +43,54 @@ export function computeGateDigest(bound) {
|
|
|
42
43
|
writeSet: normalizeWriteSet(bound.writeSet),
|
|
43
44
|
});
|
|
44
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Deterministic writeSet vs contract allowedPaths delta (2026-08-11).
|
|
48
|
+
*
|
|
49
|
+
* Classifies every normalized writeSet entry against the task contract's
|
|
50
|
+
* allowedPaths:
|
|
51
|
+
* - exactMatch: entry equals a contract allowedPath
|
|
52
|
+
* - narrowerThanContract: entry is covered by a broader contract glob
|
|
53
|
+
* (e.g. writeSet `src/worker/console/chat/**` under
|
|
54
|
+
* contract `src/**`)
|
|
55
|
+
* - additions: entry outside the contract, attributed to the
|
|
56
|
+
* exclusive writer node(s) that declared it
|
|
57
|
+
*
|
|
58
|
+
* `writers` is the list of { id, writeSet } from the DAG spec's exclusive
|
|
59
|
+
* writer tasks. The delta is a read-only field on the gate output and is
|
|
60
|
+
* NEVER included in the gate digest (AC-004: digest stays stable).
|
|
61
|
+
*/
|
|
62
|
+
export function buildWriteSetGateDelta(input) {
|
|
63
|
+
const writeSet = normalizeWriteSet(input.writeSet);
|
|
64
|
+
const contract = normalizeWriteSet(input.contractAllowedPaths);
|
|
65
|
+
const exactMatch = [];
|
|
66
|
+
const narrowerThanContract = [];
|
|
67
|
+
const additions = [];
|
|
68
|
+
for (const entry of writeSet) {
|
|
69
|
+
if (contract.includes(entry)) {
|
|
70
|
+
exactMatch.push(entry);
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
// Covered by a broader contract glob? Use the entry itself as a probe
|
|
74
|
+
// path against each contract pattern (pathMatchesPattern treats a
|
|
75
|
+
// non-glob entry as an exact-or-prefix match and glob entries via
|
|
76
|
+
// globToRegExp).
|
|
77
|
+
const covered = contract.some((contractEntry) => pathMatchesPattern(entry, contractEntry));
|
|
78
|
+
if (covered) {
|
|
79
|
+
narrowerThanContract.push(entry);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const source = (input.writers ?? [])
|
|
83
|
+
.filter((writer) => normalizeWriteSet(writer.writeSet).includes(entry))
|
|
84
|
+
.map((writer) => writer.id)
|
|
85
|
+
.sort((a, b) => a.localeCompare(b));
|
|
86
|
+
additions.push({
|
|
87
|
+
entry,
|
|
88
|
+
source,
|
|
89
|
+
coveredByContract: false,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
return { exactMatch, narrowerThanContract, additions };
|
|
93
|
+
}
|
|
45
94
|
export function buildWriteSetGate(input) {
|
|
46
95
|
const digest = computeGateDigest(input.bound);
|
|
47
96
|
const id = "write-set-review";
|
|
@@ -54,6 +103,7 @@ export function buildWriteSetGate(input) {
|
|
|
54
103
|
approveCommand: `loop-agent task advance ${input.taskId} --approve-gate ${id}:${digest} --json`,
|
|
55
104
|
rejectCommand: `loop-agent task advance ${input.taskId} --reject-gate ${id}:${digest} --reason <text> --json`,
|
|
56
105
|
bound: input.bound,
|
|
106
|
+
delta: input.delta ?? null,
|
|
57
107
|
};
|
|
58
108
|
}
|
|
59
109
|
export function parseGateToken(token) {
|
|
@@ -5,7 +5,7 @@ import { logicalRevisionForState, observeTaskContractState, } from "../../task/c
|
|
|
5
5
|
import { assessDagRunLiveness, listAllDagRunEntries, readDagRunSpec, readDagRunState, } from "../../workflows/dag/lifecycle.js";
|
|
6
6
|
import { resolveRunningControllerIdentity } from "../../shared/package-metadata.js";
|
|
7
7
|
import { loadLifecycleRecord } from "./record.js";
|
|
8
|
-
import { buildGateBinding, buildWriteSetGate, controllerIdentityAnchor, hashCanonicalPayload, normalizeWriteSet, sha256Hex, } from "./gates.js";
|
|
8
|
+
import { buildGateBinding, buildWriteSetGate, buildWriteSetGateDelta, controllerIdentityAnchor, hashCanonicalPayload, normalizeWriteSet, sha256Hex, } from "./gates.js";
|
|
9
9
|
async function exists(filePath) {
|
|
10
10
|
try {
|
|
11
11
|
await access(filePath);
|
|
@@ -263,6 +263,15 @@ export async function observeTaskLifecycle(repoRoot, taskId) {
|
|
|
263
263
|
: null),
|
|
264
264
|
})
|
|
265
265
|
: null;
|
|
266
|
+
const gateDelta = gateBound != null
|
|
267
|
+
? buildWriteSetGateDelta({
|
|
268
|
+
writeSet: gateBound.writeSet,
|
|
269
|
+
contractAllowedPaths: taskConfigAllowed,
|
|
270
|
+
writers: (parsedSpec?.tasks ?? [])
|
|
271
|
+
.filter((task) => task.writePolicy === "exclusive")
|
|
272
|
+
.map((task) => ({ id: task.id, writeSet: task.writeSet ?? [] })),
|
|
273
|
+
})
|
|
274
|
+
: null;
|
|
266
275
|
const lastGate = record?.lastGate;
|
|
267
276
|
const currentDigest = gateBound != null
|
|
268
277
|
? buildWriteSetGate({ taskId, bound: gateBound }).digest
|
|
@@ -277,7 +286,7 @@ export async function observeTaskLifecycle(repoRoot, taskId) {
|
|
|
277
286
|
// not already approved/rejected. Rejected same-digest gates stay closed
|
|
278
287
|
// until contract/source/DAG/writeSet/controller binding changes.
|
|
279
288
|
const gate = gateBound && strictValidated && !approvedCurrent && !rejectedCurrent
|
|
280
|
-
? buildWriteSetGate({ taskId, bound: gateBound })
|
|
289
|
+
? buildWriteSetGate({ taskId, bound: gateBound, delta: gateDelta })
|
|
281
290
|
: null;
|
|
282
291
|
const gateOpen = Boolean(gate);
|
|
283
292
|
let legacyState;
|
|
@@ -1023,6 +1023,7 @@ function assertPendingAcceptance(value, state, task) {
|
|
|
1023
1023
|
}
|
|
1024
1024
|
const pending = value;
|
|
1025
1025
|
const desiredSha256 = task.evidence.desiredSha256;
|
|
1026
|
+
const sourceAnchorSha256 = task.evidence.sourceAnchorSha256;
|
|
1026
1027
|
const guard = pending.guard;
|
|
1027
1028
|
const receipt = pending.receipt;
|
|
1028
1029
|
const transition = pending.transition;
|
|
@@ -1039,6 +1040,9 @@ function assertPendingAcceptance(value, state, task) {
|
|
|
1039
1040
|
!isSha256(guard.desiredSha256) ||
|
|
1040
1041
|
!isSha256(desiredSha256) ||
|
|
1041
1042
|
guard.desiredSha256 !== desiredSha256 ||
|
|
1043
|
+
(sourceAnchorSha256 === undefined
|
|
1044
|
+
? guard.sourceAnchorSha256 !== undefined
|
|
1045
|
+
: guard.sourceAnchorSha256 !== sourceAnchorSha256) ||
|
|
1042
1046
|
!isMergeReceipt(receipt) ||
|
|
1043
1047
|
receipt.taskId !== guard.taskId ||
|
|
1044
1048
|
receipt.path !== guard.path ||
|
|
@@ -1076,6 +1080,9 @@ function acceptedSurface(input) {
|
|
|
1076
1080
|
acceptedEntry.acceptedMerge = {
|
|
1077
1081
|
currentSha256: input.currentSha256,
|
|
1078
1082
|
desiredSha256: input.desiredSha256,
|
|
1083
|
+
...(typeof input.task.evidence.sourceAnchorSha256 === "string"
|
|
1084
|
+
? { sourceAnchorSha256: input.task.evidence.sourceAnchorSha256 }
|
|
1085
|
+
: {}),
|
|
1079
1086
|
};
|
|
1080
1087
|
return { beforeEntry: cloneJson(entry), accepted };
|
|
1081
1088
|
}
|
|
@@ -1210,6 +1217,7 @@ async function recordAcceptedMerge(input) {
|
|
|
1210
1217
|
if (!desiredSha256) {
|
|
1211
1218
|
throw new Error(`init upgrade cannot accept ${input.task.path}: desired hash evidence is missing`);
|
|
1212
1219
|
}
|
|
1220
|
+
const sourceAnchorSha256 = input.task.evidence.sourceAnchorSha256;
|
|
1213
1221
|
const canonicalPath = normalizeAllowedPath(input.task.path);
|
|
1214
1222
|
const surfacePath = path.join(input.state.repoRoot, INIT_SURFACE_STATE_PATH);
|
|
1215
1223
|
let pending = input.pending;
|
|
@@ -1263,6 +1271,9 @@ async function recordAcceptedMerge(input) {
|
|
|
1263
1271
|
allowedPaths: [canonicalPath],
|
|
1264
1272
|
currentSha256: input.currentSha256,
|
|
1265
1273
|
desiredSha256,
|
|
1274
|
+
...(sourceAnchorSha256 !== undefined
|
|
1275
|
+
? { sourceAnchorSha256 }
|
|
1276
|
+
: {}),
|
|
1266
1277
|
},
|
|
1267
1278
|
receipt,
|
|
1268
1279
|
transition: {
|
|
@@ -2412,7 +2423,27 @@ async function executeUpgrade(directory, state, input) {
|
|
|
2412
2423
|
});
|
|
2413
2424
|
state.actionReceipts.push(...safe.applied);
|
|
2414
2425
|
await writeJsonAtomic(path.join(directory, "change-manifest.json"), state.actionReceipts);
|
|
2415
|
-
|
|
2426
|
+
let post = await checkInitUpdate({ repoRoot: state.repoRoot });
|
|
2427
|
+
// Converge remaining safe deterministic init actions before the final check.
|
|
2428
|
+
// Project/governance-derived drift (e.g. a personalized project rename after
|
|
2429
|
+
// an accepted merge) settles here instead of failing finalCheck. Model merge
|
|
2430
|
+
// tasks and human decisions are never auto-applied.
|
|
2431
|
+
while (post.deterministicActions.length > 0) {
|
|
2432
|
+
const converged = await applyInitUpdate({
|
|
2433
|
+
repoRoot: state.repoRoot,
|
|
2434
|
+
applySafe: true,
|
|
2435
|
+
clientRecovery: "project",
|
|
2436
|
+
});
|
|
2437
|
+
state.actionReceipts.push(...converged.applied);
|
|
2438
|
+
await writeJsonAtomic(path.join(directory, "change-manifest.json"), state.actionReceipts);
|
|
2439
|
+
if (converged.applied.length === 0)
|
|
2440
|
+
break;
|
|
2441
|
+
post = await checkInitUpdate({ repoRoot: state.repoRoot });
|
|
2442
|
+
if (post.humanDecisions.length > 0 ||
|
|
2443
|
+
post.modelMergeTasks.length > 0) {
|
|
2444
|
+
break;
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
2416
2447
|
state.humanDecisions = post.humanDecisions;
|
|
2417
2448
|
if (post.humanDecisions.length > 0) {
|
|
2418
2449
|
state.mergeTasks = [];
|