@tea-agent/loop-agent 0.35.4-beta.0 → 0.36.0
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/AGENTS.md +4 -2
- package/CHANGELOG.md +55 -7
- package/dist/application/task-lifecycle/advance.js +1 -0
- package/dist/application/task-lifecycle/observe.js +15 -0
- package/dist/application/task-lifecycle/plan-transitions.js +15 -0
- package/dist/build-stamp.json +3 -3
- package/dist/commands/init-upgrade.js +351 -19
- package/dist/commands/init.js +14 -67
- package/dist/commands/run-dag-progress.js +14 -0
- package/dist/commands/task-advance.js +33 -3
- package/dist/executors/dag-pi-executor.js +3 -1
- package/dist/shared/operator/capabilities.js +125 -11
- package/dist/task/source-prepare/completeness.js +17 -0
- package/dist/task/source-prepare/parse-intent.js +15 -1
- package/dist/worker/console/chat/artifact-card.js +8 -1
- package/dist/worker/console/chat/chat-event-store.js +61 -0
- package/dist/worker/console/chat/human-gate-card.js +9 -1
- package/dist/worker/console/chat/operation-card.js +71 -2
- package/dist/worker/console/chat/pi-runtime.js +69 -30
- package/dist/worker/console/chat/routes.js +27 -4
- package/dist/worker/console/chat/semantic-activity.js +465 -0
- package/dist/worker/console/chat/turn-process.js +31 -12
- package/dist/worker/console/operation-run-facts.js +190 -0
- package/dist/worker/console/operation-runner.js +107 -6
- package/dist/worker/console/operation-wait.js +314 -0
- package/dist/worker/console/operator-actions.js +153 -2
- package/dist/worker/console/static/assets/index-2OeZODxk.js +57 -0
- package/dist/worker/console/static/assets/index-DVJlUL8X.css +1 -0
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/operator-chat/activity-journey.js +125 -0
- package/dist/worker/console/static-src/operator-chat/activity-rail-presentation.js +73 -0
- package/dist/worker/console/static-src/operator-chat/activity-references.js +20 -0
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +69 -31
- package/dist/worker/console/static-src/operator-chat/slash-palette-layout.js +24 -0
- package/dist/worker/console/static-src/operator-chat/slash-palette-nav.js +141 -0
- package/dist/worker/console/static-src/operator-chat/spatial-overlay.js +2 -1
- package/dist/worker/console/static-src/operator-chat/useActivityRailTransition.js +59 -0
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +16 -5
- package/dist/worker/console/static-src/operator-chat/useComposer.js +30 -7
- package/dist/worker/console/static-src/operator-chat/workspace-layout-mode.js +7 -3
- package/dist/worker/observe/static/operator-chrome.js +1 -1
- package/dist/workflows/dag/failure-routing.js +4 -9
- package/dist/workflows/dag/init-hybrid.js +74 -2
- package/dist/workflows/dag/lifecycle.js +0 -4
- package/dist/workflows/dag/report.js +0 -6
- package/docs/README.md +3 -3
- package/docs/architecture/evolution.md +102 -67
- package/docs/templates/frontend-task-constraints.md +7 -13
- package/docs/templates/init-managed-agents.md +5 -2
- package/harness.json +2 -2
- package/package.json +1 -1
- package/skills/loop-agent/SKILL.md +1 -0
- package/skills/loop-agent/references/command-reference.md +2 -0
- package/dist/worker/console/static/assets/index-DRqZiQ7J.css +0 -1
- package/dist/worker/console/static/assets/index-DuVLjCIT.js +0 -57
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { advanceTaskLifecycle, } from "../application/task-lifecycle/index.js";
|
|
2
2
|
import { buildOperatorResult, operatorFailed, processExitCodeForOutcome, writeOperatorJson, } from "../shared/operator/index.js";
|
|
3
|
+
import { createRunDagProgressObserver, validateRunDagProgressIntervalMs, } from "./run-dag-progress.js";
|
|
3
4
|
const COMMAND = "task advance";
|
|
4
5
|
const USAGE = `usage:
|
|
5
6
|
task advance <task-id> [title]
|
|
@@ -24,6 +25,8 @@ const USAGE = `usage:
|
|
|
24
25
|
[--dag-output <path>]
|
|
25
26
|
[--skip-finalize]
|
|
26
27
|
[--no-strict-models]
|
|
28
|
+
[--quiet]
|
|
29
|
+
[--progress-interval-ms <ms>]
|
|
27
30
|
[--dry-run]
|
|
28
31
|
[--json]`;
|
|
29
32
|
function pushList(target, value) {
|
|
@@ -170,6 +173,19 @@ export function parseTaskAdvanceArgs(args) {
|
|
|
170
173
|
options.strictModels = false;
|
|
171
174
|
continue;
|
|
172
175
|
}
|
|
176
|
+
if (token === "--quiet") {
|
|
177
|
+
options.quiet = true;
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (token === "--progress-interval-ms") {
|
|
181
|
+
const raw = next();
|
|
182
|
+
const parsed = Number(raw);
|
|
183
|
+
if (!Number.isFinite(parsed)) {
|
|
184
|
+
throw new Error(`progress-interval-ms must be an integer >= 1000\n${USAGE}`);
|
|
185
|
+
}
|
|
186
|
+
options.progressIntervalMs = validateRunDagProgressIntervalMs(parsed);
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
173
189
|
if (token === "--help" || token === "-h") {
|
|
174
190
|
throw new Error(USAGE);
|
|
175
191
|
}
|
|
@@ -208,7 +224,7 @@ function mapOutcome(result) {
|
|
|
208
224
|
return "blocked";
|
|
209
225
|
return "succeeded";
|
|
210
226
|
}
|
|
211
|
-
function toUseCaseInput(repoRoot, taskId, options) {
|
|
227
|
+
export function toUseCaseInput(repoRoot, taskId, options, observer) {
|
|
212
228
|
const timeouts = new Map((options.verifyTimeout ?? []).map((entry) => {
|
|
213
229
|
const parsed = parseVerifyTimeout(entry);
|
|
214
230
|
return [parsed.label, parsed.timeoutMs];
|
|
@@ -271,7 +287,8 @@ function toUseCaseInput(repoRoot, taskId, options) {
|
|
|
271
287
|
dagOutputPath: options.dagOutputPath,
|
|
272
288
|
skipFinalize: options.skipFinalize,
|
|
273
289
|
strictModels: options.strictModels,
|
|
274
|
-
|
|
290
|
+
...(observer ? { observer } : {}),
|
|
291
|
+
onProgress: options.quiet
|
|
275
292
|
? undefined
|
|
276
293
|
: (message) => {
|
|
277
294
|
process.stderr.write(`[task advance] ${message}\n`);
|
|
@@ -296,8 +313,18 @@ export async function runTaskAdvance(repoRoot, args) {
|
|
|
296
313
|
process.exitCode = processExitCodeForOutcome(envelope.outcome);
|
|
297
314
|
return;
|
|
298
315
|
}
|
|
316
|
+
let progress;
|
|
299
317
|
try {
|
|
300
|
-
|
|
318
|
+
// AC-006/AC-007: periodic DAG progress goes to stderr only (stdout stays
|
|
319
|
+
// the single final OperatorCommandResultV1 JSON). The observer is created
|
|
320
|
+
// ONLY for the approve-gate execution path and disposed on every exit;
|
|
321
|
+
// its timer starts only after onRunStart (double guard, no stray timer).
|
|
322
|
+
if (options.approveGate && !options.dryRun && !options.quiet) {
|
|
323
|
+
progress = createRunDagProgressObserver({
|
|
324
|
+
intervalMs: options.progressIntervalMs,
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
const result = await advanceTaskLifecycle(toUseCaseInput(repoRoot, taskId, options, progress?.observer));
|
|
301
328
|
const outcome = mapOutcome(result);
|
|
302
329
|
const gateStop = result.lifecycleState === "awaiting-write-set-approval" &&
|
|
303
330
|
result.blockers.length === 0;
|
|
@@ -333,4 +360,7 @@ export async function runTaskAdvance(repoRoot, args) {
|
|
|
333
360
|
writeOperatorJson(envelope);
|
|
334
361
|
process.exitCode = processExitCodeForOutcome(envelope.outcome);
|
|
335
362
|
}
|
|
363
|
+
finally {
|
|
364
|
+
progress?.dispose();
|
|
365
|
+
}
|
|
336
366
|
}
|
|
@@ -194,7 +194,7 @@ export function buildDagPiUserMessage(task, persona, step) {
|
|
|
194
194
|
if (isDagPiWriteTask(task)) {
|
|
195
195
|
const outcomeInstruction = task.writerOutcomePolicy
|
|
196
196
|
? [
|
|
197
|
-
"The first non-empty response line must be exactly IMPLEMENTATION_OUTCOME: changed, IMPLEMENTATION_OUTCOME: already-satisfied, or IMPLEMENTATION_OUTCOME: blocked. Use changed only after
|
|
197
|
+
"The first non-empty response line must be exactly IMPLEMENTATION_OUTCOME: changed, IMPLEMENTATION_OUTCOME: already-satisfied, or IMPLEMENTATION_OUTCOME: blocked. The runner measures your diff mechanically from git status snapshots taken before and after this node: code pasted into the response text is NOT an implementation and yields an empty diff. Use changed only after actually calling write/edit tools that persist files to disk; use already-satisfied only when the contract is already met and no file changed; use blocked when implementation cannot proceed. Claiming changed without a persisted diff fails this node as invalid-output.",
|
|
198
198
|
task.writerOutcomePolicy.requireChangedFiles
|
|
199
199
|
? "This generation node requires a non-empty bounded diff; already-satisfied cannot complete it successfully."
|
|
200
200
|
: undefined,
|
|
@@ -206,6 +206,8 @@ export function buildDagPiUserMessage(task, persona, step) {
|
|
|
206
206
|
`You are executing hybrid DAG node "${task.id}" (role=${role}, piStep=${step}, writePolicy=${writePolicy}).`,
|
|
207
207
|
"The system prompt contains the full DAG envelope: objective, constraints, upstream context, and task.",
|
|
208
208
|
"Use write tools only for the bounded implementation requested by this node.",
|
|
209
|
+
"The upstream plan node's output is a BLUEPRINT, not a deliverable: reading it never satisfies this node. You MUST materialize the plan into actual working-tree changes by calling write/edit tools before claiming changed. A detailed plan that names every file, test, or snippet still requires you to write those files yourself.",
|
|
210
|
+
"Forbidden paths bound the writeSet, not the task: when the task only requires writing inside the writeSet (e.g. adding tests that call existing public APIs), a forbidden src/** does not make the task impossible — do NOT claim blocked merely because some path you never need to write is forbidden. blocked is only for when you genuinely cannot produce the required writeSet changes.",
|
|
209
211
|
outcomeInstruction,
|
|
210
212
|
`Allowed paths: ${(task.allowedPaths ?? []).join(", ") || "(none declared)"}.`,
|
|
211
213
|
`Write set: ${(task.writeSet ?? []).join(", ") || "(none declared)"}.`,
|
|
@@ -13,6 +13,13 @@ const CONTROLLER_PROTOCOL_CAPABILITIES = [
|
|
|
13
13
|
"worker-attempt-revision-v1",
|
|
14
14
|
"recovery-decision-v2",
|
|
15
15
|
];
|
|
16
|
+
function officialModelCallable(coverage, kind) {
|
|
17
|
+
if (coverage === "human-gated-required")
|
|
18
|
+
return "prepare-only";
|
|
19
|
+
if (coverage === "advanced" && kind !== "read")
|
|
20
|
+
return "never";
|
|
21
|
+
return "always";
|
|
22
|
+
}
|
|
16
23
|
const officialAction = (action, cli, kind, coverage) => ({
|
|
17
24
|
action,
|
|
18
25
|
cli,
|
|
@@ -51,11 +58,7 @@ const officialAction = (action, cli, kind, coverage) => ({
|
|
|
51
58
|
]
|
|
52
59
|
: []),
|
|
53
60
|
],
|
|
54
|
-
modelCallable: coverage
|
|
55
|
-
? "prepare-only"
|
|
56
|
-
: coverage === "advanced" && kind !== "read"
|
|
57
|
-
? "never"
|
|
58
|
-
: "always",
|
|
61
|
+
modelCallable: officialModelCallable(coverage, kind),
|
|
59
62
|
humanConfirmation: coverage === "human-gated-required" ? "required" : "none",
|
|
60
63
|
});
|
|
61
64
|
const OFFICIAL_ACTIONS = [
|
|
@@ -142,13 +145,13 @@ const OFFICIAL_ACTIONS = [
|
|
|
142
145
|
["stats", "loop-agent stats", "read", "model-callable"],
|
|
143
146
|
[
|
|
144
147
|
"promoteRun",
|
|
145
|
-
"loop-agent task advance
|
|
148
|
+
"loop-agent task advance",
|
|
146
149
|
"mutation",
|
|
147
150
|
"model-callable",
|
|
148
151
|
],
|
|
149
152
|
[
|
|
150
153
|
"closeoutTask",
|
|
151
|
-
"loop-agent task advance
|
|
154
|
+
"loop-agent task advance",
|
|
152
155
|
"mutation",
|
|
153
156
|
"model-callable",
|
|
154
157
|
],
|
|
@@ -445,6 +448,49 @@ export function buildOperatorCapabilitiesDocument() {
|
|
|
445
448
|
modelCallable: "always",
|
|
446
449
|
humanConfirmation: "none",
|
|
447
450
|
},
|
|
451
|
+
{
|
|
452
|
+
action: "operationWait",
|
|
453
|
+
cli: "console canonical operation wait (read-only event-driven long poll)",
|
|
454
|
+
kind: "read",
|
|
455
|
+
inputSchemaVersion: 1,
|
|
456
|
+
resultSchemaVersion: 1,
|
|
457
|
+
resultPolicy: { readOnly: true, bounded: true, redacted: true },
|
|
458
|
+
envelopeSchemaVersion: 1,
|
|
459
|
+
requiredErrorCodes: [
|
|
460
|
+
"NOT_FOUND",
|
|
461
|
+
"INVALID_INPUT",
|
|
462
|
+
"EVENT_CURSOR_EXPIRED",
|
|
463
|
+
],
|
|
464
|
+
description: "Read-only event-driven wait on the canonical operation event ring. Default wakeOn=meaningful: heartbeats never settle the wait and are never returned as event payload (nextSeq still covers them); state/result/stdout/stderr/reconcile/error settle immediately; terminal or needs-reconcile flushes immediately. Every subsequent wait MUST pass afterSeq equal to the previous response nextSeq; omitting or reusing an older cursor replays already-consumed startup events. The response includes recommendedNextCall with the safe cursor. wakeOn=all (diagnostic compatibility) resolves on the first new event including heartbeats. maxWaitMs elapsing returns timedOut:true (a success summary, not a command failure).",
|
|
465
|
+
inputParams: [
|
|
466
|
+
{
|
|
467
|
+
name: "operationId",
|
|
468
|
+
type: "string",
|
|
469
|
+
required: true,
|
|
470
|
+
description: "canonical operation id",
|
|
471
|
+
},
|
|
472
|
+
{
|
|
473
|
+
name: "afterSeq",
|
|
474
|
+
type: "number",
|
|
475
|
+
required: false,
|
|
476
|
+
description: "event cursor; first wait may use 0, every later wait MUST use the previous response nextSeq (>= 0)",
|
|
477
|
+
},
|
|
478
|
+
{
|
|
479
|
+
name: "maxWaitMs",
|
|
480
|
+
type: "number",
|
|
481
|
+
required: false,
|
|
482
|
+
description: "bounded wait budget; clamped server-side (default 180000, floor 60000)",
|
|
483
|
+
},
|
|
484
|
+
{
|
|
485
|
+
name: "wakeOn",
|
|
486
|
+
type: "string",
|
|
487
|
+
required: false,
|
|
488
|
+
description: "wake policy: meaningful (default; heartbeats are cursor-only liveness) | all (first event settles, diagnostics)",
|
|
489
|
+
},
|
|
490
|
+
],
|
|
491
|
+
modelCallable: "always",
|
|
492
|
+
humanConfirmation: "none",
|
|
493
|
+
},
|
|
448
494
|
{
|
|
449
495
|
action: "contractShow",
|
|
450
496
|
cli: "loop-agent task status <taskId> --json",
|
|
@@ -896,6 +942,74 @@ export function buildOperatorCapabilitiesDocument() {
|
|
|
896
942
|
modelCallable: "always",
|
|
897
943
|
humanConfirmation: "none",
|
|
898
944
|
},
|
|
945
|
+
{
|
|
946
|
+
action: "taskAdvance",
|
|
947
|
+
cli: "loop-agent task advance <taskId> [title] --task-kind <kind> --allowed-path <glob> --forbidden-path <glob> --verify <label:command> --json",
|
|
948
|
+
kind: "long-running",
|
|
949
|
+
inputSchemaVersion: 1,
|
|
950
|
+
resultSchemaVersion: 1,
|
|
951
|
+
envelopeSchemaVersion: 1,
|
|
952
|
+
requiredErrorCodes: [
|
|
953
|
+
...COMMON_MUTATION_ERRORS,
|
|
954
|
+
"INVALID_INPUT",
|
|
955
|
+
],
|
|
956
|
+
description: "Canonical typed task advance mutation. Use after task creation/PRD import to apply explicit taskKind and engineering boundaries, generate + strict-validate the DAG, and stop at the writeSet gate. Never pass placeholder ids such as <id>. A successful child process may still return a blocked business outcome; inspect lifecycleState, blockers, gate, and next.",
|
|
957
|
+
inputParams: [
|
|
958
|
+
{
|
|
959
|
+
name: "taskId",
|
|
960
|
+
type: "string",
|
|
961
|
+
required: true,
|
|
962
|
+
description: "canonical YYYY-MM-DD-<slug> task id; placeholders are rejected",
|
|
963
|
+
},
|
|
964
|
+
{
|
|
965
|
+
name: "title",
|
|
966
|
+
type: "string",
|
|
967
|
+
required: false,
|
|
968
|
+
description: "optional task title for create/first advance",
|
|
969
|
+
},
|
|
970
|
+
{
|
|
971
|
+
name: "taskKind",
|
|
972
|
+
type: "string",
|
|
973
|
+
required: false,
|
|
974
|
+
description: "business DAG template; ordinary fixes/docs use standard",
|
|
975
|
+
},
|
|
976
|
+
{
|
|
977
|
+
name: "featureId",
|
|
978
|
+
type: "string",
|
|
979
|
+
required: false,
|
|
980
|
+
description: "required only by feature-scoped task kinds such as knowledge-sync",
|
|
981
|
+
},
|
|
982
|
+
{
|
|
983
|
+
name: "profile",
|
|
984
|
+
type: "string",
|
|
985
|
+
required: false,
|
|
986
|
+
description: "governance profile auto|minimal|standard|reviewed|supervised",
|
|
987
|
+
},
|
|
988
|
+
{
|
|
989
|
+
name: "allowedPaths",
|
|
990
|
+
type: "array",
|
|
991
|
+
itemsType: "string",
|
|
992
|
+
required: false,
|
|
993
|
+
description: "explicit writer boundary globs",
|
|
994
|
+
},
|
|
995
|
+
{
|
|
996
|
+
name: "forbiddenPaths",
|
|
997
|
+
type: "array",
|
|
998
|
+
itemsType: "string",
|
|
999
|
+
required: false,
|
|
1000
|
+
description: "explicit forbidden path globs",
|
|
1001
|
+
},
|
|
1002
|
+
{
|
|
1003
|
+
name: "verifyCommands",
|
|
1004
|
+
type: "array",
|
|
1005
|
+
itemsType: "string",
|
|
1006
|
+
required: false,
|
|
1007
|
+
description: "verification entries as label:command strings",
|
|
1008
|
+
},
|
|
1009
|
+
],
|
|
1010
|
+
modelCallable: "always",
|
|
1011
|
+
humanConfirmation: "none",
|
|
1012
|
+
},
|
|
899
1013
|
{
|
|
900
1014
|
action: "dagRunTask",
|
|
901
1015
|
cli: "loop-agent task advance <taskId> --profile auto --dag-output <path> --json",
|
|
@@ -965,7 +1079,7 @@ export function buildOperatorCapabilitiesDocument() {
|
|
|
965
1079
|
"CONTROLLER_MISMATCH",
|
|
966
1080
|
"INVALID_INPUT",
|
|
967
1081
|
],
|
|
968
|
-
description: "Consume a single-use execution receipt and execute the reviewed DAG (prefer task advance --approve-gate when gate token present). accepted/queued/running/operationId are NOT completion — supervise via operationGet/status/dagReport/dagDoctor.",
|
|
1082
|
+
description: "Consume a single-use execution receipt and execute the reviewed DAG (prefer task advance --approve-gate when gate token present). accepted/queued/running/operationId are NOT completion — supervise via operationGet/status/dagReport/dagDoctor/operationWait.",
|
|
969
1083
|
inputParams: [
|
|
970
1084
|
{
|
|
971
1085
|
name: "executionId",
|
|
@@ -988,7 +1102,7 @@ export function buildOperatorCapabilitiesDocument() {
|
|
|
988
1102
|
resultSchemaVersion: 1,
|
|
989
1103
|
envelopeSchemaVersion: 1,
|
|
990
1104
|
requiredErrorCodes: ["INVALID_INPUT", "NOT_FOUND"],
|
|
991
|
-
description: "Read-only R1 subgraph plan (no mutation). Call before dagRerun. Prefer primaryFailure.nodeId from dagReport. If eligible=false (writer/decision/fingerprint), do not force execute — fall back to standaloneTaskRerun or same-task advance. Returns planHash required by execute.",
|
|
1105
|
+
description: "Read-only R1 subgraph plan (no mutation). Call before dagRerun. Prefer primaryFailure.nodeId from dagReport. If eligible=false (writer/decision/fingerprint, or subgraph contains unsafe shell nodes), do not force execute — fall back to standaloneTaskRerun or same-task advance after fixing the root cause (contract changes must be adopted first). Returns planHash required by execute.",
|
|
992
1106
|
inputParams: [
|
|
993
1107
|
{
|
|
994
1108
|
name: "runId",
|
|
@@ -1317,7 +1431,7 @@ export function buildOperatorCapabilitiesDocument() {
|
|
|
1317
1431
|
"INVALID_INPUT",
|
|
1318
1432
|
"HUMAN_CONFIRMATION_REQUIRED",
|
|
1319
1433
|
],
|
|
1320
|
-
description: "Full standalone task regenerate → validate → execute with parent lineage. Only when node rerun plan is ineligible or primaryRecovery recommends rerun-task; the server checks read-only run facts before accepting.",
|
|
1434
|
+
description: "Full standalone task regenerate → validate → execute with parent lineage. Only when node rerun plan is ineligible or primaryRecovery recommends rerun-task; the server checks read-only run facts before accepting. Regeneration reuses the CURRENT contract snapshot: if the failure requires contract changes (e.g. allowing a catalog index path), first update the contract (re-import PRD, or adopt externally-edited sources via `task contract adopt`) BEFORE calling this action, otherwise the regenerated DAG repeats the same failure.",
|
|
1321
1435
|
inputParams: [
|
|
1322
1436
|
{
|
|
1323
1437
|
name: "runId",
|
|
@@ -1438,7 +1552,7 @@ export const OPERATOR_COMMAND_COVERAGE = Object.freeze([
|
|
|
1438
1552
|
{
|
|
1439
1553
|
command: "loop-agent task advance",
|
|
1440
1554
|
coverage: "model-callable",
|
|
1441
|
-
action: "
|
|
1555
|
+
action: "taskAdvance",
|
|
1442
1556
|
source: "loop-agent",
|
|
1443
1557
|
},
|
|
1444
1558
|
{
|
|
@@ -2,6 +2,13 @@ const FRESH_SOURCE_MISSING_CODES = new Set([
|
|
|
2
2
|
"MISSING_REQUIREMENT",
|
|
3
3
|
"MISSING_CONSTRAINTS",
|
|
4
4
|
]);
|
|
5
|
+
function hasKnowledgeSyncFeatureSurface(featureId, allowedPaths) {
|
|
6
|
+
const featureRoot = `features/${featureId}/`;
|
|
7
|
+
return allowedPaths.some((pathGlob) => {
|
|
8
|
+
const normalized = pathGlob.replaceAll("\\", "/").replace(/^\.\//, "");
|
|
9
|
+
return normalized === `features/${featureId}` || normalized.startsWith(featureRoot);
|
|
10
|
+
});
|
|
11
|
+
}
|
|
5
12
|
export function isFreshLegacyUnversioned(state, sourceFilesPresent) {
|
|
6
13
|
if (state.effectiveStatus !== "legacy-unversioned")
|
|
7
14
|
return false;
|
|
@@ -105,6 +112,16 @@ export function listPrepareGaps(input) {
|
|
|
105
112
|
message: "featureId is required when taskKind is knowledge-sync",
|
|
106
113
|
});
|
|
107
114
|
}
|
|
115
|
+
if (draft.taskKind === "knowledge-sync" &&
|
|
116
|
+
draft.featureId &&
|
|
117
|
+
!hasKnowledgeSyncFeatureSurface(draft.featureId, draft.constraints.allowedPaths)) {
|
|
118
|
+
gaps.push({
|
|
119
|
+
code: "KNOWLEDGE_SYNC_PATH_MISMATCH",
|
|
120
|
+
level: "blocking",
|
|
121
|
+
field: "allowedPaths",
|
|
122
|
+
message: `knowledge-sync for ${draft.featureId} requires an allowed path under features/${draft.featureId}/; ordinary docs-only work should use taskKind standard`,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
108
125
|
if (draft.requirement.scope.length === 0) {
|
|
109
126
|
gaps.push({
|
|
110
127
|
code: "EMPTY_SCOPE",
|
|
@@ -15,7 +15,21 @@ const HEADING_ALIASES = {
|
|
|
15
15
|
"已确认需求",
|
|
16
16
|
]),
|
|
17
17
|
nonGoals: new Set(["非目标", "out of scope", "non-goals", "non goals"]),
|
|
18
|
-
acceptance: new Set([
|
|
18
|
+
acceptance: new Set([
|
|
19
|
+
"验收标准",
|
|
20
|
+
"acceptance criteria",
|
|
21
|
+
"acceptance",
|
|
22
|
+
// Common zh-CN product-writing variants seen in real PRDs; without
|
|
23
|
+
// these a PRD re-import flips to deterministic parse and loses all AC
|
|
24
|
+
// (EMPTY_ACCEPTANCE deadlock on the pendingPrdRefresh path).
|
|
25
|
+
"完成定义",
|
|
26
|
+
"完成标准",
|
|
27
|
+
"验收",
|
|
28
|
+
"验收条件",
|
|
29
|
+
"验收准则",
|
|
30
|
+
"dod",
|
|
31
|
+
"definition of done",
|
|
32
|
+
]),
|
|
19
33
|
constraints: new Set(["约束", "constraints", "不变式", "invariants"]),
|
|
20
34
|
openQuestions: new Set(["待决问题", "open questions", "open-questions"]),
|
|
21
35
|
assumptions: new Set(["假设", "assumptions"]),
|
|
@@ -19,5 +19,12 @@ export function mergeArtifactCardState(current, incoming) {
|
|
|
19
19
|
const index = current.findIndex((item) => item.artifactId === incoming.artifactId);
|
|
20
20
|
if (index < 0)
|
|
21
21
|
return [...current, incoming];
|
|
22
|
-
return current.map((item, candidate) => candidate === index
|
|
22
|
+
return current.map((item, candidate) => candidate === index
|
|
23
|
+
? {
|
|
24
|
+
...item,
|
|
25
|
+
...incoming,
|
|
26
|
+
eventSeq: item.eventSeq ?? incoming.eventSeq,
|
|
27
|
+
activityRef: item.activityRef ?? incoming.activityRef,
|
|
28
|
+
}
|
|
29
|
+
: item);
|
|
23
30
|
}
|
|
@@ -50,6 +50,62 @@ function controllerSummary(operation) {
|
|
|
50
50
|
.join("@");
|
|
51
51
|
return safeIdentity(label);
|
|
52
52
|
}
|
|
53
|
+
/** Re-project an already-safe stored run summary through a bounded re-check. */
|
|
54
|
+
function safeRunSummaryProjection(value) {
|
|
55
|
+
if (!value || typeof value !== "object")
|
|
56
|
+
return undefined;
|
|
57
|
+
const record = value;
|
|
58
|
+
const runId = safeIdentity(record.runId);
|
|
59
|
+
const status = safeIdentity(record.status);
|
|
60
|
+
if (!runId || !status)
|
|
61
|
+
return undefined;
|
|
62
|
+
const nodes = Array.isArray(record.nodes)
|
|
63
|
+
? record.nodes
|
|
64
|
+
.filter((node) => Boolean(node && typeof node === "object"))
|
|
65
|
+
.map((node) => ({
|
|
66
|
+
id: safeIdentity(node.id) ?? "",
|
|
67
|
+
status: safeIdentity(node.status) ?? "",
|
|
68
|
+
}))
|
|
69
|
+
.filter((node) => node.id && node.status)
|
|
70
|
+
.slice(0, 20)
|
|
71
|
+
: [];
|
|
72
|
+
const verdict = typeof record.verdict === "string"
|
|
73
|
+
? boundedRedactedText(record.verdict)
|
|
74
|
+
: undefined;
|
|
75
|
+
const failureCategory = typeof record.failureCategory === "string"
|
|
76
|
+
? boundedRedactedText(record.failureCategory)
|
|
77
|
+
: undefined;
|
|
78
|
+
const outOfBounds = Array.isArray(record.outOfBounds)
|
|
79
|
+
? record.outOfBounds
|
|
80
|
+
.filter((entry) => Boolean(entry && typeof entry === "object"))
|
|
81
|
+
.map((entry) => ({
|
|
82
|
+
nodeId: safeIdentity(entry.nodeId) ?? "",
|
|
83
|
+
failureCategory: safeIdentity(entry.failureCategory) ?? "",
|
|
84
|
+
}))
|
|
85
|
+
.filter((entry) => entry.nodeId && entry.failureCategory)
|
|
86
|
+
.slice(0, 20)
|
|
87
|
+
: undefined;
|
|
88
|
+
const nextRec = record.next && typeof record.next === "object"
|
|
89
|
+
? record.next
|
|
90
|
+
: undefined;
|
|
91
|
+
const next = nextRec
|
|
92
|
+
? {
|
|
93
|
+
kind: safeIdentity(nextRec.kind) ?? "",
|
|
94
|
+
description: typeof nextRec.description === "string"
|
|
95
|
+
? boundedRedactedText(nextRec.description)
|
|
96
|
+
: undefined,
|
|
97
|
+
}
|
|
98
|
+
: undefined;
|
|
99
|
+
return {
|
|
100
|
+
runId,
|
|
101
|
+
status,
|
|
102
|
+
nodes,
|
|
103
|
+
...(verdict ? { verdict } : {}),
|
|
104
|
+
...(failureCategory ? { failureCategory } : {}),
|
|
105
|
+
...(outOfBounds && outOfBounds.length > 0 ? { outOfBounds } : {}),
|
|
106
|
+
...(next && next.kind ? { next } : {}),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
53
109
|
/** Chat/browser-safe projection. Never includes actionParams, CLI args, paths or raw results. */
|
|
54
110
|
export function projectOperationForChat(operation) {
|
|
55
111
|
const params = operation.actionParams ?? {};
|
|
@@ -61,12 +117,16 @@ export function projectOperationForChat(operation) {
|
|
|
61
117
|
const featureId = safeIdentity(params.featureId);
|
|
62
118
|
const workerRunId = safeIdentity(params.workerRunId);
|
|
63
119
|
const controller = controllerSummary(operation);
|
|
120
|
+
const runSummary = safeRunSummaryProjection(operation.runSummary);
|
|
64
121
|
const previewHash = createHash("sha256")
|
|
65
122
|
.update(JSON.stringify({
|
|
66
123
|
stdoutPreview,
|
|
67
124
|
stderrPreview,
|
|
68
125
|
summaryPreview,
|
|
69
126
|
state: operation.state,
|
|
127
|
+
// runSummary participates in the dedupe hash: terminal summary updates
|
|
128
|
+
// must not be swallowed by state+previewHash dedup (2026-08-14).
|
|
129
|
+
runSummary: runSummary ?? null,
|
|
70
130
|
}))
|
|
71
131
|
.digest("hex");
|
|
72
132
|
return {
|
|
@@ -88,6 +148,7 @@ export function projectOperationForChat(operation) {
|
|
|
88
148
|
...(operation.errorCode
|
|
89
149
|
? { errorCode: safeIdentity(operation.errorCode) }
|
|
90
150
|
: {}),
|
|
151
|
+
...(runSummary ? { runSummary } : {}),
|
|
91
152
|
previewHash,
|
|
92
153
|
};
|
|
93
154
|
}
|
|
@@ -32,6 +32,14 @@ export function mergeHumanGateCardState(cards, next) {
|
|
|
32
32
|
if (index < 0)
|
|
33
33
|
return [...cards, next];
|
|
34
34
|
const copy = [...cards];
|
|
35
|
-
copy[index]
|
|
35
|
+
const current = copy[index];
|
|
36
|
+
if (!current)
|
|
37
|
+
return [...cards, next];
|
|
38
|
+
copy[index] = {
|
|
39
|
+
...current,
|
|
40
|
+
...next,
|
|
41
|
+
eventSeq: current.eventSeq ?? next.eventSeq,
|
|
42
|
+
activityRef: current.activityRef ?? next.activityRef,
|
|
43
|
+
};
|
|
36
44
|
return copy;
|
|
37
45
|
}
|
|
@@ -2,7 +2,24 @@ export function mergeOperationCardState(current, incoming) {
|
|
|
2
2
|
const index = current.findIndex((operation) => operation.operationId === incoming.operationId);
|
|
3
3
|
if (index < 0)
|
|
4
4
|
return [...current, incoming];
|
|
5
|
-
return current.map((operation, candidateIndex) =>
|
|
5
|
+
return current.map((operation, candidateIndex) => {
|
|
6
|
+
if (candidateIndex !== index)
|
|
7
|
+
return operation;
|
|
8
|
+
let eventSeq = operation.eventSeq;
|
|
9
|
+
if (eventSeq === undefined)
|
|
10
|
+
eventSeq = incoming.eventSeq;
|
|
11
|
+
else if (incoming.eventSeq !== undefined) {
|
|
12
|
+
eventSeq = Math.min(eventSeq, incoming.eventSeq);
|
|
13
|
+
}
|
|
14
|
+
return {
|
|
15
|
+
...operation,
|
|
16
|
+
...incoming,
|
|
17
|
+
...(eventSeq === undefined ? {} : { eventSeq }),
|
|
18
|
+
updatedSeq: Math.max(operation.updatedSeq ?? operation.eventSeq ?? 0, incoming.updatedSeq ?? incoming.eventSeq ?? 0),
|
|
19
|
+
toolCallId: operation.toolCallId ?? incoming.toolCallId,
|
|
20
|
+
activityRef: operation.activityRef ?? incoming.activityRef,
|
|
21
|
+
};
|
|
22
|
+
});
|
|
6
23
|
}
|
|
7
24
|
export function operationFromEventPayload(payload) {
|
|
8
25
|
const operation = payload.operation;
|
|
@@ -19,5 +36,57 @@ export function operationFromEventPayload(payload) {
|
|
|
19
36
|
typeof candidate.previewHash !== "string") {
|
|
20
37
|
return undefined;
|
|
21
38
|
}
|
|
22
|
-
|
|
39
|
+
// Optional runSummary stays backward-compatible: validate shape field by
|
|
40
|
+
// field and drop it entirely when malformed (legacy events simply lack it).
|
|
41
|
+
const safe = safeRunSummary(candidate.runSummary);
|
|
42
|
+
const { runSummary: _unsafeRunSummary, ...base } = candidate;
|
|
43
|
+
return {
|
|
44
|
+
...base,
|
|
45
|
+
...(safe ? { runSummary: safe } : {}),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/** Field-by-field validation for the optional browser-side runSummary. */
|
|
49
|
+
function safeRunSummary(value) {
|
|
50
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
51
|
+
return undefined;
|
|
52
|
+
const rec = value;
|
|
53
|
+
if (typeof rec.runId !== "string" || !rec.runId)
|
|
54
|
+
return undefined;
|
|
55
|
+
if (typeof rec.status !== "string" || !rec.status)
|
|
56
|
+
return undefined;
|
|
57
|
+
let nodes = [];
|
|
58
|
+
if (Array.isArray(rec.nodes)) {
|
|
59
|
+
nodes = rec.nodes
|
|
60
|
+
.flatMap((node) => {
|
|
61
|
+
if (!node || typeof node !== "object")
|
|
62
|
+
return [];
|
|
63
|
+
const id = String(node.id ?? "");
|
|
64
|
+
const status = String(node.status ?? "");
|
|
65
|
+
if (!id || !status)
|
|
66
|
+
return [];
|
|
67
|
+
return [{ id, status }];
|
|
68
|
+
})
|
|
69
|
+
.slice(0, 20);
|
|
70
|
+
}
|
|
71
|
+
let next;
|
|
72
|
+
if (rec.next && typeof rec.next === "object") {
|
|
73
|
+
const nextRecord = rec.next;
|
|
74
|
+
next = {
|
|
75
|
+
kind: String(nextRecord.kind ?? ""),
|
|
76
|
+
...(typeof nextRecord.description === "string"
|
|
77
|
+
? { description: nextRecord.description }
|
|
78
|
+
: {}),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
const out = {
|
|
82
|
+
runId: rec.runId,
|
|
83
|
+
status: rec.status,
|
|
84
|
+
nodes,
|
|
85
|
+
...(typeof rec.verdict === "string" ? { verdict: rec.verdict } : {}),
|
|
86
|
+
...(typeof rec.failureCategory === "string"
|
|
87
|
+
? { failureCategory: rec.failureCategory }
|
|
88
|
+
: {}),
|
|
89
|
+
...(next && next.kind ? { next } : {}),
|
|
90
|
+
};
|
|
91
|
+
return out;
|
|
23
92
|
}
|