pi-goal-list-loop-audit 0.38.47 → 0.38.49
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 +52 -0
- package/INSTALL.md +2 -2
- package/README.md +7 -5
- package/docs/DESIGN-long-running-supervision.md +9 -4
- package/docs/INDEX.md +1 -1
- package/extensions/approval-render-store.ts +4 -0
- package/extensions/completion-summary.ts +11 -12
- package/extensions/glla-update-check.ts +5 -0
- package/extensions/goal-loop-core.ts +14 -0
- package/extensions/loops/goal-tools.ts +155 -42
- package/extensions/task-batch.ts +156 -0
- package/package.json +1 -1
- package/prompts/goal-loop-continuation.md +6 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,57 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.38.49 — audit-pass drift fixes: prompt gate, summary segmentation, task honesty (2026-09-11)
|
|
4
|
+
|
|
5
|
+
Full-project audit pass on v0.38.48 (three parallel scout surveys). Six new
|
|
6
|
+
findings, all fixed on main; scout claims that were unproven, stale, or
|
|
7
|
+
by-design are disposed with rationale in the pass note, not as findings.
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- **Continuation prompt drift:** the Available-tools list and task workflow
|
|
12
|
+
now name `update_task_batch` (validate whole, verify milestones, persist
|
|
13
|
+
once) and the pending-task completion gate (`complete_goal` refused
|
|
14
|
+
before audit while committed tasks are open; `record_goal_judgment`
|
|
15
|
+
`choice=deferred` + `taskId` exempts a blocked item). Completion-voice
|
|
16
|
+
guidance matches the rich-section renderer budgets.
|
|
17
|
+
- **README verify path:** the `complete_goal` steps now include the
|
|
18
|
+
pending-task refusal before audit.
|
|
19
|
+
- **Docs versions/voice:** INSTALL updating examples show the current
|
|
20
|
+
versions; the supervision design doc distinguishes the verbatim six-label
|
|
21
|
+
archive record from the rich chat/transcript sections and the compact
|
|
22
|
+
width-bound projection.
|
|
23
|
+
- **Summary gate segmentation:** `missingCompletionSummaryLabels` segments
|
|
24
|
+
on the same last-occurrence positions as every projector, so a label
|
|
25
|
+
named inside an earlier value can no longer pass the gate while rendering
|
|
26
|
+
shifted (removed the now-dead `labelIndex`).
|
|
27
|
+
- **Task-tool honesty:** completing an already-complete task reports
|
|
28
|
+
`Task <id> is already complete.` and same-status `update_task_status`
|
|
29
|
+
moves report `already <status>` instead of the not-found fallthrough or
|
|
30
|
+
a no-op persist.
|
|
31
|
+
- **Sidecar pending boundary:** the approval-render outbox and the
|
|
32
|
+
update-check refresh defer while sessionDir resolution is pending instead
|
|
33
|
+
of creating (or latching degraded on) a `<cwd>/.pi-glla` fallback tree.
|
|
34
|
+
|
|
35
|
+
### Tests
|
|
36
|
+
|
|
37
|
+
- Re-baselined `tests/context-growth-measurement.test.ts` to 22_570 chars
|
|
38
|
+
/ 22_670 bytes per payload (+769 chars incl. one em dash) with exact
|
|
39
|
+
multiples preserved across probe sizes. New pins: stolen-label gate
|
|
40
|
+
agreement, already-complete/no-op task messages, sidecar pending deferral.
|
|
41
|
+
`tsc` clean.
|
|
42
|
+
|
|
43
|
+
## 0.38.48 — atomic task batches + pending-task completion gate (2026-09-11)
|
|
44
|
+
|
|
45
|
+
### Added
|
|
46
|
+
|
|
47
|
+
- **update_task_batch tool:** validate whole → verify every `complete` milestone's `verificationContract` → copy + apply → persist once. A rejected batch or a mid-batch milestone failure leaves state untouched.
|
|
48
|
+
- **Pending-task completion gate:** `complete_goal` with open committed tasks is refused BEFORE the auditor, naming each open item (`- <id>: "<title>" (<status>)`) with a `complete_goal_tasks_refused` ledger entry. Refusal mutates nothing and cannot be bypassed via `newObjective` (gate sits before the pivot branch).
|
|
49
|
+
- **Deferral exemption:** `record_goal_judgment` accepts `taskId` only with `choice=deferred`; stamps `task.deferred = { reason, followUp, at }` in the same persist as the ledger entry. `inline` + `taskId` and unknown ids are refused with no ledger change.
|
|
50
|
+
|
|
51
|
+
### Tests
|
|
52
|
+
|
|
53
|
+
- `tests/task-atomicity-gate.test.ts`: 7 pure-core + 10 MockPi pins (mid-batch kill untouched, refusal names each task, deferral pass-through). `tsc` clean.
|
|
54
|
+
|
|
3
55
|
## 0.38.47 — layered prompts: skeleton plus on-demand detail (2026-09-11)
|
|
4
56
|
|
|
5
57
|
### Changed
|
package/INSTALL.md
CHANGED
|
@@ -31,11 +31,11 @@ GLLA loads into new pi sessions. If pi is already open, reload that session:
|
|
|
31
31
|
|
|
32
32
|
## Updating
|
|
33
33
|
|
|
34
|
-
The status line always shows the running version (`glla: … · v0.38.
|
|
34
|
+
The status line always shows the running version (`glla: … · v0.38.48`).
|
|
35
35
|
When the npm registry is ahead, it also nudges:
|
|
36
36
|
|
|
37
37
|
```text
|
|
38
|
-
glla: … · v0.38.
|
|
38
|
+
glla: … · v0.38.47 · update v0.38.48 available
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
The nudge comes from a daily sidecar check (`.pi-glla/update-check.json`),
|
package/README.md
CHANGED
|
@@ -250,12 +250,14 @@ When the agent calls `complete_goal`, GLLA:
|
|
|
250
250
|
|
|
251
251
|
1. runs the contract's mechanical checks when a release or command check is
|
|
252
252
|
specified;
|
|
253
|
-
2.
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
253
|
+
2. refuses the claim before audit when committed tasks are still open
|
|
254
|
+
(tasks with a recorded deferral are exempt), naming each open task;
|
|
255
|
+
3. writes an identity-bound completion claim;
|
|
256
|
+
4. starts a detached, fresh pi RPC worker for the audit;
|
|
257
|
+
5. asks the worker to inspect the repository and run bounded checks;
|
|
258
|
+
6. requires raw evidence for each verification-contract item through the
|
|
257
259
|
orchestrator-side regression shield;
|
|
258
|
-
|
|
260
|
+
7. keeps the goal open on infrastructure failure, missing evidence, or
|
|
259
261
|
disapproval instead of silently archiving it.
|
|
260
262
|
|
|
261
263
|
The auditor is intentionally isolated from the implementing conversation and
|
|
@@ -110,10 +110,15 @@ status/reason, durable telemetry, captured audit verdicts, and known archive
|
|
|
110
110
|
path. It says `not recorded` when a changed-file manifest or test result is not
|
|
111
111
|
available. It never infers a passing test or invents a commit.
|
|
112
112
|
|
|
113
|
-
The full recap lives in the archive
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
113
|
+
The full six-label recap lives verbatim in the archive (`## Completion summary`)
|
|
114
|
+
and the status/history surfaces. The human layer in chat, transcript, and the
|
|
115
|
+
archive's `## Terminal summary` renders the same facts as rich sections
|
|
116
|
+
(`## Done`, Key Findings, Verification Summary table, Next, capped and clipped
|
|
117
|
+
to fixed budgets). Width-bound and external surfaces (status line, widget card,
|
|
118
|
+
external notifies) use the compact single-line projection of all six labels.
|
|
119
|
+
Every terminal goal notification, including version-bearing already-shipped claims,
|
|
120
|
+
explicit goal/list cancellation, and `/glla wipe`, carries either the rich sections
|
|
121
|
+
or the compact projection; loop notifications do the same. The terminal
|
|
117
122
|
notification may use a compact excerpt. The executor recap and independent
|
|
118
123
|
auditor verdict stay separate: an approval is not manufactured from the
|
|
119
124
|
presence of a summary.
|
package/docs/INDEX.md
CHANGED
|
@@ -16,7 +16,7 @@ Policy contracts and recent changes live in the `audit/` directory of the
|
|
|
16
16
|
failback; v0.35.9 hardened cross-version npm tarball checks; v0.35.10
|
|
17
17
|
handles multi-entry npm dry-run reports; v0.35.11 accepts both npm report
|
|
18
18
|
shapes; v0.35.12 supports npm 12's keyed pack reports; v0.35.13 fixes stale-API recovery loops.
|
|
19
|
-
v0.35.14–v0.38.
|
|
19
|
+
v0.35.14–v0.38.49 continue through the supervisor freeze (`/glla pause`),
|
|
20
20
|
load hold, auditor picker parity, Windows launch fix, zombie-watchdog
|
|
21
21
|
subagent carve-out, due-wait backstop, the `/glla agents` visibility panel,
|
|
22
22
|
durable state-root selection, blank-until-resume auditor context, frozen
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { appendLedger, ensureDirs, nowIso, piGlaDir, runPersistStep } from "./goal-loop-core.js";
|
|
4
|
+
import { stateRootPending } from "./glla-state-root.js";
|
|
4
5
|
|
|
5
6
|
/** Durable terminal-summary outbox. A toast or a live host is not delivery:
|
|
6
7
|
* only a confirmed visible session message acknowledges a render. */
|
|
@@ -67,6 +68,9 @@ function readRenders(cwd: string): PendingApprovalRender[] {
|
|
|
67
68
|
}
|
|
68
69
|
|
|
69
70
|
function writeRenders(cwd: string, renders: PendingApprovalRender[]): boolean {
|
|
71
|
+
// v0.38.49 audit: pending sessionDir resolution defers every write that
|
|
72
|
+
// would otherwise recreate <cwd>/.pi-glla — the sidecar is no exception.
|
|
73
|
+
if (stateRootPending()) return false;
|
|
70
74
|
const landed = runPersistStep("persistApprovalRender", () => {
|
|
71
75
|
ensureDirs(cwd);
|
|
72
76
|
const file = approvalRenderStorePath(cwd);
|
|
@@ -31,10 +31,6 @@ export interface CompletionSummaryFacts {
|
|
|
31
31
|
archivePath?: string;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
function labelIndex(text: string, label: CompletionSummaryLabel): number {
|
|
35
|
-
return text.toLowerCase().indexOf(label.toLowerCase());
|
|
36
|
-
}
|
|
37
|
-
|
|
38
34
|
/**
|
|
39
35
|
* Validation/version annotations are metadata, not recap fields. Keep them
|
|
40
36
|
* out of the parser so a generated NOTE containing examples such as
|
|
@@ -45,17 +41,20 @@ function completionSummaryBody(text: string): string {
|
|
|
45
41
|
return annotation?.index === undefined ? text : text.slice(0, annotation.index);
|
|
46
42
|
}
|
|
47
43
|
|
|
48
|
-
/** Return labels that are absent or have no value after the label.
|
|
44
|
+
/** Return labels that are absent or have no value after the label. Segments on
|
|
45
|
+
* the same last-occurrence positions as every projector (compact, multi-line,
|
|
46
|
+
* rich): a label named inside an earlier value must not shift the gate away
|
|
47
|
+
* from what the user will actually read. */
|
|
49
48
|
export function missingCompletionSummaryLabels(text: string): CompletionSummaryLabel[] {
|
|
50
49
|
const normalized = completionSummaryBody(text).trim();
|
|
50
|
+
const positions = labelPositions(normalized.toLowerCase());
|
|
51
51
|
return COMPLETION_SUMMARY_LABELS.filter((label) => {
|
|
52
|
-
const
|
|
53
|
-
if (
|
|
54
|
-
const valueStart = start + label.length;
|
|
55
|
-
const next =
|
|
56
|
-
.
|
|
57
|
-
.
|
|
58
|
-
.map((index) => valueStart + index)
|
|
52
|
+
const current = positions.find((entry) => entry.label === label);
|
|
53
|
+
if (!current) return true;
|
|
54
|
+
const valueStart = current.start + label.length;
|
|
55
|
+
const next = positions
|
|
56
|
+
.filter((entry) => entry.start > current.start)
|
|
57
|
+
.map((entry) => entry.start)
|
|
59
58
|
.sort((a, b) => a - b)[0];
|
|
60
59
|
return normalized.slice(valueStart, next ?? normalized.length).trim().length === 0;
|
|
61
60
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
+
import { stateRootPending } from "./glla-state-root.js";
|
|
4
5
|
import {
|
|
5
6
|
compareVersions,
|
|
6
7
|
GLLA_PACKAGE_NAME,
|
|
@@ -60,6 +61,10 @@ export function refreshUpdateCheck(
|
|
|
60
61
|
now = Date.now(),
|
|
61
62
|
spawnFn: SpawnFn = spawn as unknown as SpawnFn,
|
|
62
63
|
): void {
|
|
64
|
+
// v0.38.49 audit: while sessionDir resolution is pending there is no
|
|
65
|
+
// session root yet — skip the refresh (and its cache write) instead of
|
|
66
|
+
// creating a <cwd>/.pi-glla fallback tree.
|
|
67
|
+
if (stateRootPending()) return;
|
|
63
68
|
try {
|
|
64
69
|
const cached = readUpdateCheckRaw(cwd);
|
|
65
70
|
if (cached && now - cached.checkedAt < UPDATE_CHECK_TTL_MS) return;
|
|
@@ -97,6 +97,15 @@ export function workCommand(mode: Policy | "loop" | undefined, command: string):
|
|
|
97
97
|
return `${workCommandRoot(mode)} ${command}`;
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
export interface TaskDeferral {
|
|
101
|
+
/** Why the durable action is unsafe, impossible, or blocked right now. */
|
|
102
|
+
reason: string;
|
|
103
|
+
/** The bounded durable follow-up. */
|
|
104
|
+
followUp: string;
|
|
105
|
+
/** ISO timestamp of the recorded judgment. */
|
|
106
|
+
at: string;
|
|
107
|
+
}
|
|
108
|
+
|
|
100
109
|
export interface Task {
|
|
101
110
|
id: string;
|
|
102
111
|
title: string;
|
|
@@ -106,6 +115,11 @@ export interface Task {
|
|
|
106
115
|
/** Optional verification gate for milestone-checked tasks. */
|
|
107
116
|
verificationContract?: string;
|
|
108
117
|
subtasks?: Task[];
|
|
118
|
+
/** A recorded deferral (via record_goal_judgment choice=deferred + taskId)
|
|
119
|
+
* exempts this task from the complete_goal pending-task gate. The
|
|
120
|
+
* exemption is durable state, not prose: the ledger holds the judgment,
|
|
121
|
+
* this stamp is the gate-readable pointer. */
|
|
122
|
+
deferred?: TaskDeferral;
|
|
109
123
|
}
|
|
110
124
|
|
|
111
125
|
export interface TaskList {
|
|
@@ -134,6 +134,16 @@ isGoalRevisionCurrent,
|
|
|
134
134
|
buildDurableChoiceRecord,
|
|
135
135
|
normalizeDurableDeferRecommendationInput,
|
|
136
136
|
} from "../goal-loop-core.js";
|
|
137
|
+
import {
|
|
138
|
+
applyValidatedBatch,
|
|
139
|
+
collectOpenTasks,
|
|
140
|
+
findTask,
|
|
141
|
+
formatOpenTaskRefusal,
|
|
142
|
+
patchTaskCopy,
|
|
143
|
+
validateTaskBatch,
|
|
144
|
+
withTaskStatus,
|
|
145
|
+
type TaskStatusUpdate,
|
|
146
|
+
} from "../task-batch.js";
|
|
137
147
|
import {
|
|
138
148
|
createContinuationDispatch,
|
|
139
149
|
dispatchMatchesOwner,
|
|
@@ -556,6 +566,22 @@ function registerAgentTools(pi: any): void {
|
|
|
556
566
|
details: {},
|
|
557
567
|
};
|
|
558
568
|
}
|
|
569
|
+
// v0.38.48: pending-task completion gate — a claim with open
|
|
570
|
+
// committed tasks is refused BEFORE the auditor, naming each open
|
|
571
|
+
// item. Tasks carrying a recorded deferral (record_goal_judgment
|
|
572
|
+
// choice=deferred + taskId) are exempt. Placed before newObjective:
|
|
573
|
+
// a pivot must not silently orphan committed tasks — finish them or
|
|
574
|
+
// defer them explicitly first. Refusal mutates nothing.
|
|
575
|
+
if (state.goal.taskList) {
|
|
576
|
+
const openTasks = collectOpenTasks(state.goal.taskList);
|
|
577
|
+
if (openTasks.length > 0) {
|
|
578
|
+
appendLedger(ctx.cwd, "complete_goal_tasks_refused", {
|
|
579
|
+
goalId: state.goal.id,
|
|
580
|
+
open: openTasks.map((t) => ({ id: t.id, title: t.title, status: t.status })),
|
|
581
|
+
});
|
|
582
|
+
return { content: [{ type: "text", text: formatOpenTaskRefusal(openTasks) }], details: {} };
|
|
583
|
+
}
|
|
584
|
+
}
|
|
559
585
|
// v0.25.0 (contract item 15): atomic objective update + audit in one
|
|
560
586
|
// call — the objective-drift disapprove loop (ship shifted work →
|
|
561
587
|
// auditor disapproves the ORIGINAL objective) ends here. Ledgered so
|
|
@@ -1913,6 +1939,7 @@ function registerAgentTools(pi: any): void {
|
|
|
1913
1939
|
durableFix: Type.Optional(Type.String({ maxLength: 500, description: "The maintainable root-cause action to show in the goal card" })),
|
|
1914
1940
|
deferRecommendations: Type.Optional(Type.Array(Type.String({ maxLength: 500 }), { maxItems: 8, description: "Earlier bounded workaround/defer recommendations to retain as UI evidence" })),
|
|
1915
1941
|
durableBlocked: Type.Optional(Type.Boolean({ description: "True only when the durable action is unsafe, impossible, or blocked for this turn" })),
|
|
1942
|
+
taskId: Type.Optional(Type.String({ description: "v0.38.48: with choice=deferred, attach this deferral to a task so the complete_goal pending-task gate exempts it. Only valid with choice=deferred." })),
|
|
1916
1943
|
}),
|
|
1917
1944
|
async execute(_id, params, signal, _onUpdate, execCtx) {
|
|
1918
1945
|
const foreignJudgment = foreignToolGuard(execCtx);
|
|
@@ -1930,7 +1957,20 @@ function registerAgentTools(pi: any): void {
|
|
|
1930
1957
|
durableFix?: string;
|
|
1931
1958
|
deferRecommendations?: string[];
|
|
1932
1959
|
durableBlocked?: boolean;
|
|
1960
|
+
taskId?: string;
|
|
1933
1961
|
};
|
|
1962
|
+
// v0.38.48: a task-linked deferral exempts that task from the
|
|
1963
|
+
// complete_goal pending-task gate. Inline needs no exemption (the fix
|
|
1964
|
+
// ships now); an unknown task is refused before anything is recorded.
|
|
1965
|
+
const taskId = p.taskId?.trim() || undefined;
|
|
1966
|
+
if (taskId && p.choice !== "deferred") {
|
|
1967
|
+
return { content: [{ type: "text", text: "taskId is only valid with choice=deferred — an inline judgment implements the fix now and needs no task exemption. Nothing was recorded." }], details: {} };
|
|
1968
|
+
}
|
|
1969
|
+
const goal = state.goal;
|
|
1970
|
+
const deferredTask = taskId ? (goal.taskList ? findTask(goal.taskList, taskId) : null) : undefined;
|
|
1971
|
+
if (taskId && !deferredTask) {
|
|
1972
|
+
return { content: [{ type: "text", text: `No task "${taskId}" in this goal's task list — the judgment was NOT recorded.` }], details: {} };
|
|
1973
|
+
}
|
|
1934
1974
|
const record = buildDurableChoiceRecord(p.choice, p.reason, p.followUp);
|
|
1935
1975
|
if (!record.reason) {
|
|
1936
1976
|
return { content: [{ type: "text", text: "A non-empty reason is required to record a durable-vs-defer judgment." }], details: {} };
|
|
@@ -1938,19 +1978,30 @@ function registerAgentTools(pi: any): void {
|
|
|
1938
1978
|
if (record.choice === "deferred" && !record.followUp) {
|
|
1939
1979
|
return { content: [{ type: "text", text: "A deferred judgment also requires a durable follow-up." }], details: {} };
|
|
1940
1980
|
}
|
|
1941
|
-
const goal = state.goal;
|
|
1942
1981
|
const durableDeferRecommendation = durableDeferFactsForGoal(goal, record, p);
|
|
1943
1982
|
const landed = appendLedger(ctx.cwd, "durable_defer_choice", {
|
|
1944
1983
|
goalId: goal.id,
|
|
1945
1984
|
...record,
|
|
1985
|
+
...(taskId ? { taskId, taskTitle: deferredTask!.title } : {}),
|
|
1946
1986
|
durableDeferRecommendation,
|
|
1947
1987
|
});
|
|
1948
1988
|
if (!landed) {
|
|
1949
1989
|
return { content: [{ type: "text", text: "The durable-vs-defer judgment could not be persisted; no choice was recorded." }], details: {} };
|
|
1950
1990
|
}
|
|
1951
|
-
|
|
1991
|
+
// v0.38.48: the task stamp rides the SAME persist as the UI
|
|
1992
|
+
// projection — one write, never a stamped-but-unprojected split.
|
|
1993
|
+
// record.followUp is proven non-empty for deferred above; a taskId
|
|
1994
|
+
// always implies deferred (refused otherwise), so the non-null
|
|
1995
|
+
// assertion holds exactly when a stamp is written.
|
|
1996
|
+
const patch: Record<string, unknown> = { durableDeferRecommendation };
|
|
1997
|
+
if (taskId && goal.taskList) {
|
|
1998
|
+
patch.taskList = patchTaskCopy(goal.taskList, taskId, {
|
|
1999
|
+
deferred: { reason: record.reason, followUp: record.followUp!, at: nowIso() },
|
|
2000
|
+
});
|
|
2001
|
+
}
|
|
2002
|
+
if (!updateGoal(patch as Parameters<typeof updateGoal>[0], ctx)) {
|
|
1952
2003
|
return {
|
|
1953
|
-
content: [{ type: "text", text: `Recorded durable-vs-defer judgment: ${record.choice}, but its
|
|
2004
|
+
content: [{ type: "text", text: `Recorded durable-vs-defer judgment: ${record.choice} in the ledger, but its persistence failed${taskId ? " — the task exemption did NOT land, so the gate will still block" : ""}. Retry.` }],
|
|
1954
2005
|
details: {},
|
|
1955
2006
|
};
|
|
1956
2007
|
}
|
|
@@ -1959,7 +2010,7 @@ function registerAgentTools(pi: any): void {
|
|
|
1959
2010
|
// from the state ledger through Goal.durableDeferRecommendation.
|
|
1960
2011
|
refreshUI(ctx, true);
|
|
1961
2012
|
return {
|
|
1962
|
-
content: [{ type: "text", text: `Recorded durable-vs-defer judgment: ${record.choice}.` }],
|
|
2013
|
+
content: [{ type: "text", text: `Recorded durable-vs-defer judgment: ${record.choice}.${taskId ? ` Task ${taskId} ("${deferredTask!.title}") is now exempt from the complete_goal pending-task gate.` : ""}` }],
|
|
1963
2014
|
details: {},
|
|
1964
2015
|
};
|
|
1965
2016
|
},
|
|
@@ -2160,71 +2211,133 @@ function registerAgentTools(pi: any): void {
|
|
|
2160
2211
|
return { content: [{ type: "text", text: "No task list in this goal." }], details: {} };
|
|
2161
2212
|
}
|
|
2162
2213
|
const tl = state.goal.taskList;
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2214
|
+
// v0.38.48: copy-swap — the live list is never mutated in place, so a
|
|
2215
|
+
// kill between verify and persist (or a persist failure) cannot leave
|
|
2216
|
+
// partial state. Message strings are pinned by
|
|
2217
|
+
// tests/task-atomicity-gate.test.ts.
|
|
2218
|
+
const t = findTask(tl, p.id);
|
|
2219
|
+
if (!t) return { content: [{ type: "text", text: `Task ${p.id} not found.` }], details: {} };
|
|
2220
|
+
// v0.38.49 audit: an existing but already-complete task is a state
|
|
2221
|
+
// fact, not a typo — report it instead of the not-found fallthrough.
|
|
2222
|
+
if (t.status === "complete") return { content: [{ type: "text", text: `Task ${p.id} is already complete.` }], details: {} };
|
|
2223
|
+
if (t) {
|
|
2224
|
+
const checkRes = await verifyTaskMilestone(ctx, t.verificationContract, signal);
|
|
2225
|
+
if (checkRes) {
|
|
2226
|
+
return {
|
|
2227
|
+
content: [{
|
|
2228
|
+
type: "text",
|
|
2229
|
+
text: `Task ${p.id} milestone verification FAILED for command \`${checkRes.failedCommand}\` (exit code ${checkRes.exitCode}):\n\n${checkRes.output}\n\nTask ${p.id} remains in_progress. Fix the failure before marking complete.`,
|
|
2230
|
+
}],
|
|
2231
|
+
details: {},
|
|
2232
|
+
};
|
|
2233
|
+
}
|
|
2234
|
+
if (!updateGoal({ taskList: withTaskStatus(tl, p.id, "complete") }, ctx)) {
|
|
2235
|
+
return { content: [{ type: "text", text: `Task ${p.id} could not be marked complete — the persist failed and no state changed. Retry.` }], details: {} };
|
|
2236
|
+
}
|
|
2237
|
+
return { content: [{ type: "text", text: `Task ${p.id} marked complete.` }], details: {} };
|
|
2238
|
+
}
|
|
2239
|
+
},
|
|
2240
|
+
}));
|
|
2241
|
+
|
|
2242
|
+
pi.registerTool(defineTool({
|
|
2243
|
+
name: "update_task_status",
|
|
2244
|
+
label: "Update task status",
|
|
2245
|
+
description: "Update a task's status (pending/in_progress/complete).",
|
|
2246
|
+
parameters: Type.Object({
|
|
2247
|
+
id: Type.String(),
|
|
2248
|
+
status: Type.Union([Type.Literal("pending"), Type.Literal("in_progress"), Type.Literal("complete")]),
|
|
2249
|
+
}),
|
|
2250
|
+
async execute(_id, params, signal, _onUpdate, execCtx) {
|
|
2251
|
+
const foreign8 = foreignToolGuard(execCtx);
|
|
2252
|
+
if (foreign8) return { content: [{ type: "text", text: foreign8 }], details: {} };
|
|
2253
|
+
const ctx = currentToolContext(execCtx);
|
|
2254
|
+
if (!ctx) return staleToolResult();
|
|
2255
|
+
const p = params as { id: string; status: "pending" | "in_progress" | "complete" };
|
|
2256
|
+
if (!state.goal || !state.goal.taskList) {
|
|
2257
|
+
return { content: [{ type: "text", text: "No task list in this goal." }], details: {} };
|
|
2258
|
+
}
|
|
2259
|
+
const tl = state.goal.taskList;
|
|
2260
|
+
// v0.38.48: copy-swap (see complete_task). Message strings are pinned
|
|
2261
|
+
// by tests/task-atomicity-gate.test.ts.
|
|
2262
|
+
const t = findTask(tl, p.id);
|
|
2263
|
+
if (!t) return { content: [{ type: "text", text: `Task ${p.id} not found.` }], details: {} };
|
|
2264
|
+
// v0.38.49 audit: same-status moves are no-ops with a state-fact
|
|
2265
|
+
// reply, not a persist round-trip reporting a change that happened.
|
|
2266
|
+
if (t.status === p.status) return { content: [{ type: "text", text: `Task ${p.id} is already ${p.status}.` }], details: {} };
|
|
2267
|
+
if (t) {
|
|
2268
|
+
if (p.status === "complete") {
|
|
2167
2269
|
const checkRes = await verifyTaskMilestone(ctx, t.verificationContract, signal);
|
|
2168
2270
|
if (checkRes) {
|
|
2169
2271
|
return {
|
|
2170
2272
|
content: [{
|
|
2171
2273
|
type: "text",
|
|
2172
|
-
text: `Task ${p.id} milestone verification FAILED for command \`${checkRes.failedCommand}\` (exit code ${checkRes.exitCode}):\n\n${checkRes.output}\n\nTask ${p.id} remains
|
|
2274
|
+
text: `Task ${p.id} milestone verification FAILED for command \`${checkRes.failedCommand}\` (exit code ${checkRes.exitCode}):\n\n${checkRes.output}\n\nTask ${p.id} remains ${t.status}. Fix the failure before marking complete.`,
|
|
2173
2275
|
}],
|
|
2174
2276
|
details: {},
|
|
2175
2277
|
};
|
|
2176
2278
|
}
|
|
2177
|
-
t.status = "complete";
|
|
2178
|
-
updateGoal({ taskList: tl }, ctx);
|
|
2179
|
-
return { content: [{ type: "text", text: `Task ${p.id} marked complete.` }], details: {} };
|
|
2180
2279
|
}
|
|
2181
|
-
if (
|
|
2280
|
+
if (!updateGoal({ taskList: withTaskStatus(tl, p.id, p.status) }, ctx)) {
|
|
2281
|
+
return { content: [{ type: "text", text: `Task ${p.id} could not move to ${p.status} — the persist failed and no state changed. Retry.` }], details: {} };
|
|
2282
|
+
}
|
|
2283
|
+
return { content: [{ type: "text", text: `Task ${p.id} → ${p.status}` }], details: {} };
|
|
2182
2284
|
}
|
|
2183
|
-
return { content: [{ type: "text", text: `Task ${p.id} not found.` }], details: {} };
|
|
2184
2285
|
},
|
|
2185
2286
|
}));
|
|
2186
2287
|
|
|
2187
2288
|
pi.registerTool(defineTool({
|
|
2188
|
-
name: "
|
|
2189
|
-
label: "Update task
|
|
2190
|
-
description: "
|
|
2289
|
+
name: "update_task_batch",
|
|
2290
|
+
label: "Update task batch",
|
|
2291
|
+
description: "Atomically update several tasks at once: the whole batch is validated first, milestones are verified, then all statuses persist in one write. Any failure rejects the batch and leaves state untouched.",
|
|
2191
2292
|
parameters: Type.Object({
|
|
2192
|
-
|
|
2193
|
-
|
|
2293
|
+
updates: Type.Array(Type.Object({
|
|
2294
|
+
id: Type.String({ description: "Task id to update" }),
|
|
2295
|
+
status: Type.Union([Type.Literal("pending"), Type.Literal("in_progress"), Type.Literal("complete")]),
|
|
2296
|
+
}), { description: "Status updates to apply atomically (each task id at most once)" }),
|
|
2194
2297
|
}),
|
|
2195
2298
|
async execute(_id, params, signal, _onUpdate, execCtx) {
|
|
2196
|
-
const
|
|
2197
|
-
if (
|
|
2299
|
+
const foreignBatch = foreignToolGuard(execCtx);
|
|
2300
|
+
if (foreignBatch) return { content: [{ type: "text", text: foreignBatch }], details: {} };
|
|
2198
2301
|
const ctx = currentToolContext(execCtx);
|
|
2199
2302
|
if (!ctx) return staleToolResult();
|
|
2200
|
-
const p = params as {
|
|
2303
|
+
const p = params as { updates: TaskStatusUpdate[] };
|
|
2201
2304
|
if (!state.goal || !state.goal.taskList) {
|
|
2202
2305
|
return { content: [{ type: "text", text: "No task list in this goal." }], details: {} };
|
|
2203
2306
|
}
|
|
2204
2307
|
const tl = state.goal.taskList;
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
}
|
|
2308
|
+
// Phase 1 — validate the WHOLE batch before any verification runs.
|
|
2309
|
+
const validation = validateTaskBatch(tl, p.updates);
|
|
2310
|
+
if (!validation.ok) {
|
|
2311
|
+
return { content: [{ type: "text", text: validation.error }], details: {} };
|
|
2312
|
+
}
|
|
2313
|
+
// Phase 2 — verify every milestone moving to complete. All failures
|
|
2314
|
+
// are collected so one bad milestone does not hide another; NOTHING
|
|
2315
|
+
// is applied until every verification passes (mid-batch kill here
|
|
2316
|
+
// leaves state unchanged — the copy below does not exist yet).
|
|
2317
|
+
const failures: string[] = [];
|
|
2318
|
+
for (const e of validation.entries) {
|
|
2319
|
+
if (e.status === "complete" && e.task.status !== "complete") {
|
|
2320
|
+
const checkRes = await verifyTaskMilestone(ctx, e.task.verificationContract, signal);
|
|
2321
|
+
if (checkRes) {
|
|
2322
|
+
failures.push(`Task ${e.task.id} milestone verification FAILED for command \`${checkRes.failedCommand}\` (exit code ${checkRes.exitCode}):\n\n${checkRes.output}`);
|
|
2220
2323
|
}
|
|
2221
|
-
t.status = p.status;
|
|
2222
|
-
updateGoal({ taskList: tl }, ctx);
|
|
2223
|
-
return { content: [{ type: "text", text: `Task ${p.id} → ${p.status}` }], details: {} };
|
|
2224
2324
|
}
|
|
2225
|
-
if (t.subtasks) queue.push(...t.subtasks);
|
|
2226
2325
|
}
|
|
2227
|
-
|
|
2326
|
+
if (failures.length > 0) {
|
|
2327
|
+
return {
|
|
2328
|
+
content: [{
|
|
2329
|
+
type: "text",
|
|
2330
|
+
text: `Task batch rejected — ${failures.length} milestone verification${failures.length === 1 ? "" : "s"} failed. No task was changed:\n\n${failures.join("\n\n")}`,
|
|
2331
|
+
}],
|
|
2332
|
+
details: {},
|
|
2333
|
+
};
|
|
2334
|
+
}
|
|
2335
|
+
// Phase 3 — copy, apply, persist ONCE.
|
|
2336
|
+
if (!updateGoal({ taskList: applyValidatedBatch(tl, validation.entries) }, ctx)) {
|
|
2337
|
+
return { content: [{ type: "text", text: "Task batch could not persist — the write failed and no state changed. Retry." }], details: {} };
|
|
2338
|
+
}
|
|
2339
|
+
const applied = validation.entries.map((e) => `${e.task.id} → ${e.status}`).join(", ");
|
|
2340
|
+
return { content: [{ type: "text", text: `Task batch applied (${validation.entries.length}): ${applied}.` }], details: {} };
|
|
2228
2341
|
},
|
|
2229
2342
|
}));
|
|
2230
2343
|
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// Task-batch atomicity core (v0.38.48).
|
|
2
|
+
//
|
|
3
|
+
// The single-task tools used to mutate the live task list in place and then
|
|
4
|
+
// persist — a mid-batch kill (or a late verification failure) left partial
|
|
5
|
+
// state behind with no record of what was intended. This module is the whole
|
|
6
|
+
// mutation discipline:
|
|
7
|
+
//
|
|
8
|
+
// validate whole (ids resolve, no duplicates) → verify milestones →
|
|
9
|
+
// copy + apply → persist once.
|
|
10
|
+
//
|
|
11
|
+
// Everything here is pure except the injected verify/persist callbacks, so
|
|
12
|
+
// the contract is pinnable without a harness: a rejected batch returns the
|
|
13
|
+
// ORIGINAL list object untouched, and the tools persist exactly once.
|
|
14
|
+
|
|
15
|
+
import type { Task, TaskList } from "./goal-loop-core.js";
|
|
16
|
+
|
|
17
|
+
export type TaskStatus = "pending" | "in_progress" | "complete";
|
|
18
|
+
|
|
19
|
+
export interface TaskStatusUpdate {
|
|
20
|
+
id: string;
|
|
21
|
+
status: TaskStatus;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ValidatedTaskUpdate {
|
|
25
|
+
task: Task;
|
|
26
|
+
status: TaskStatus;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type BatchValidation =
|
|
30
|
+
| { ok: true; entries: ValidatedTaskUpdate[] }
|
|
31
|
+
| { ok: false; error: string };
|
|
32
|
+
|
|
33
|
+
/** Breadth-first search over top-level tasks and subtasks, in list order. */
|
|
34
|
+
export function findTask(taskList: TaskList, id: string): Task | null {
|
|
35
|
+
const queue: Task[] = [...taskList.tasks];
|
|
36
|
+
while (queue.length > 0) {
|
|
37
|
+
const t = queue.shift()!;
|
|
38
|
+
if (t.id === id) return t;
|
|
39
|
+
if (t.subtasks) queue.push(...t.subtasks);
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Validate the WHOLE batch before anything is verified, copied, or
|
|
46
|
+
* persisted. Every id must resolve and appear exactly once; statuses must
|
|
47
|
+
* be legal. Any failure names the offending entry and changes nothing.
|
|
48
|
+
*/
|
|
49
|
+
export function validateTaskBatch(
|
|
50
|
+
taskList: TaskList,
|
|
51
|
+
updates: TaskStatusUpdate[],
|
|
52
|
+
): BatchValidation {
|
|
53
|
+
if (!Array.isArray(updates) || updates.length === 0) {
|
|
54
|
+
return { ok: false, error: "Task batch is empty — pass at least one {id, status} update." };
|
|
55
|
+
}
|
|
56
|
+
const seen = new Set<string>();
|
|
57
|
+
const entries: ValidatedTaskUpdate[] = [];
|
|
58
|
+
for (const u of updates) {
|
|
59
|
+
if (!u || typeof u.id !== "string" || !u.id) {
|
|
60
|
+
return { ok: false, error: "Task batch rejected — every update needs a non-empty string id. No task was changed." };
|
|
61
|
+
}
|
|
62
|
+
if (u.status !== "pending" && u.status !== "in_progress" && u.status !== "complete") {
|
|
63
|
+
return { ok: false, error: `Task batch rejected — update for \"${u.id}\" carries an illegal status ${JSON.stringify((u as { status: unknown }).status)}. No task was changed.` };
|
|
64
|
+
}
|
|
65
|
+
if (seen.has(u.id)) {
|
|
66
|
+
return { ok: false, error: `Task batch rejected — duplicate update for task \"${u.id}\" in one batch (each task may appear once). No task was changed.` };
|
|
67
|
+
}
|
|
68
|
+
seen.add(u.id);
|
|
69
|
+
const task = findTask(taskList, u.id);
|
|
70
|
+
if (!task) {
|
|
71
|
+
return { ok: false, error: `Task batch rejected — task \"${u.id}\" not found. No task was changed.` };
|
|
72
|
+
}
|
|
73
|
+
entries.push({ task, status: u.status });
|
|
74
|
+
}
|
|
75
|
+
return { ok: true, entries };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Apply validated entries to a DEEP COPY. The input list is never mutated,
|
|
80
|
+
* so a kill between copy and persist (or a persist failure) leaves the
|
|
81
|
+
* caller's state object exactly as it was.
|
|
82
|
+
*/
|
|
83
|
+
export function applyValidatedBatch(
|
|
84
|
+
taskList: TaskList,
|
|
85
|
+
entries: ValidatedTaskUpdate[],
|
|
86
|
+
): TaskList {
|
|
87
|
+
const next: TaskList = JSON.parse(JSON.stringify(taskList)) as TaskList;
|
|
88
|
+
for (const e of entries) {
|
|
89
|
+
const target = findTask(next, e.task.id);
|
|
90
|
+
// Proven by validateTaskBatch against the same shape — a missing target
|
|
91
|
+
// here would mean the list changed under us; fail loudly, never partial.
|
|
92
|
+
if (!target) throw new Error(`Task batch apply failed — task \"${e.task.id}\" vanished mid-batch. No persist was attempted.`);
|
|
93
|
+
target.status = e.status;
|
|
94
|
+
}
|
|
95
|
+
return next;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Copy with an arbitrary patch merged into one task; the input list is untouched. */
|
|
99
|
+
export function patchTaskCopy(
|
|
100
|
+
taskList: TaskList,
|
|
101
|
+
id: string,
|
|
102
|
+
patch: Partial<Task>,
|
|
103
|
+
): TaskList {
|
|
104
|
+
const next: TaskList = JSON.parse(JSON.stringify(taskList)) as TaskList;
|
|
105
|
+
const target = findTask(next, id);
|
|
106
|
+
if (!target) throw new Error(`patchTaskCopy: task "${id}" not found.`);
|
|
107
|
+
Object.assign(target, patch);
|
|
108
|
+
return next;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Copy with a single task's status replaced; the input list is untouched. */
|
|
112
|
+
export function withTaskStatus(
|
|
113
|
+
taskList: TaskList,
|
|
114
|
+
id: string,
|
|
115
|
+
status: TaskStatus,
|
|
116
|
+
): TaskList {
|
|
117
|
+
const task = findTask(taskList, id);
|
|
118
|
+
if (!task) throw new Error(`withTaskStatus: task \"${id}\" not found.`);
|
|
119
|
+
return applyValidatedBatch(taskList, [{ task, status }]);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export interface OpenTaskInfo {
|
|
123
|
+
id: string;
|
|
124
|
+
title: string;
|
|
125
|
+
status: TaskStatus;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Every committed task (top-level and subtask) still open, in list order.
|
|
130
|
+
* Complete tasks and tasks carrying a recorded deferral are not open — a
|
|
131
|
+
* deferral is an explicit durable-vs-defer judgment, not abandonment.
|
|
132
|
+
*/
|
|
133
|
+
export function collectOpenTasks(taskList: TaskList): OpenTaskInfo[] {
|
|
134
|
+
const open: OpenTaskInfo[] = [];
|
|
135
|
+
const queue: Task[] = [...taskList.tasks];
|
|
136
|
+
while (queue.length > 0) {
|
|
137
|
+
const t = queue.shift()!;
|
|
138
|
+
if (t.status !== "complete" && !t.deferred) {
|
|
139
|
+
open.push({ id: t.id, title: t.title, status: t.status });
|
|
140
|
+
}
|
|
141
|
+
if (t.subtasks) queue.push(...t.subtasks);
|
|
142
|
+
}
|
|
143
|
+
return open;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** The refusal text for the complete_goal pending-task gate. Names every open task. */
|
|
147
|
+
export function formatOpenTaskRefusal(open: OpenTaskInfo[]): string {
|
|
148
|
+
const lines = open.map((t) => `- ${t.id}: \"${t.title}\" (${t.status})`);
|
|
149
|
+
return (
|
|
150
|
+
`complete_goal REFUSED — ${open.length} open committed task${open.length === 1 ? " is" : "s are"} still unfinished:\n` +
|
|
151
|
+
lines.join("\n") +
|
|
152
|
+
`\n\nFinish them with complete_task or update_task_batch, or record an explicit ` +
|
|
153
|
+
`deferral for a genuinely blocked item with record_goal_judgment ` +
|
|
154
|
+
`(choice=\"deferred\", taskId=\"<id>\"). The claim was NOT sent to the auditor and no state changed.`
|
|
155
|
+
);
|
|
156
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-goal-list-loop-audit",
|
|
3
|
-
"version": "0.38.
|
|
3
|
+
"version": "0.38.49",
|
|
4
4
|
"description": "Mission control for autonomous pi: interview-drafted goals, an audited task queue, and forever-loops (metric, spec, project-audit) that run for hours. A detached extension-less auditor process re-verifies every completion with raw evidence without holding the main pi turn; confirmed drafts, decision pauses and consent gates keep you in charge.",
|
|
5
5
|
"license": "AGPL-3.0-only",
|
|
6
6
|
"author": "dracon",
|
|
@@ -55,9 +55,9 @@ When a goal, list item, or pending task explicitly says `Agent: Designer`, `Role
|
|
|
55
55
|
|
|
56
56
|
## Available tools
|
|
57
57
|
|
|
58
|
-
You have `read`, `write`, `edit`, `bash`, `grep`, `find`, `ls`, the `subagent` tool, and the goal toolkit (`propose_task_list`, `complete_task`, `update_task_status`, `record_goal_judgment`, `pause_goal`, `complete_goal`), plus the list tools (`list_add`, `list_status`, `list_activate`) — when the user asks to queue more work ("add these to my list", "queue these 10 things"), call `list_add` with the items; when unsure what is running or waiting, call `list_status`.
|
|
58
|
+
You have `read`, `write`, `edit`, `bash`, `grep`, `find`, `ls`, the `subagent` tool, and the goal toolkit (`propose_task_list`, `complete_task`, `update_task_status`, `update_task_batch`, `record_goal_judgment`, `pause_goal`, `complete_goal`), plus the list tools (`list_add`, `list_status`, `list_activate`) — when the user asks to queue more work ("add these to my list", "queue these 10 things"), call `list_add` with the items; when unsure what is running or waiting, call `list_status`.
|
|
59
59
|
|
|
60
|
-
If the objective decomposes into milestones and no task list exists yet, call `propose_task_list` early — the user confirms it, then you track progress with `complete_task` / `update_task_status` as you go (not
|
|
60
|
+
If the objective decomposes into milestones and no task list exists yet, call `propose_task_list` early — the user confirms it, then you track progress with `complete_task` / `update_task_status` / `update_task_batch` as you go (not as a post-hoc checklist at the end). For several status changes at once, prefer one `update_task_batch` call: the whole batch validates first, milestones verify, then all statuses persist in one write. Limits: 20 tasks, 5 subtasks per task.
|
|
61
61
|
|
|
62
62
|
When the agent calls any of these, the orchestrator tracks the call and persists state to `.pi-glla/active.jsonl`.
|
|
63
63
|
|
|
@@ -156,6 +156,8 @@ Do NOT conclude "the loop is too eager" or "I am broken" before checking what a
|
|
|
156
156
|
|
|
157
157
|
Use tasks as PROGRESS TRACKERS during your work — not as a post-hoc checklist to batch-mark at the end.
|
|
158
158
|
|
|
159
|
+
Pending-task gate: `complete_goal` is refused BEFORE the auditor when committed tasks are still open — the refusal names each open task and sends nothing to the auditor. Finish them with `complete_task` / `update_task_batch`, or record an explicit deferral with `record_goal_judgment` (`choice="deferred"` + `taskId`) for a genuinely blocked item.
|
|
160
|
+
|
|
159
161
|
Before deciding that the goal is achieved, perform a completion audit against the actual current state:
|
|
160
162
|
|
|
161
163
|
- Restate the objective as concrete deliverables or success criteria.
|
|
@@ -174,7 +176,7 @@ recap and synthesizes a recorded-facts-only fallback at terminalization when a
|
|
|
174
176
|
legacy or incomplete caller omits it; never invent a commit, changed file, or
|
|
175
177
|
passing test to make the recap sound complete.
|
|
176
178
|
|
|
177
|
-
Then call `complete_goal
|
|
179
|
+
Then call `complete_goal` — only with every committed task finished or explicitly deferred, otherwise the claim is refused before audit (see the pending-task gate above). The orchestrator will spawn an **isolated auditor** in a fresh session to verify, and either accept (mark goal complete) or reject (continue work).
|
|
178
180
|
|
|
179
181
|
If your work has shifted to items different from the original objective (the original was blocked, higher-ROI items emerged): pass `newObjective` to `complete_goal` to atomically update the objective and audit against the NEW one — do NOT call `complete_goal` on the original objective after shipping different work, the auditor will disapprove because the original isn't shipped. Alternatively `pause_goal` proposing a `/goal tweak` if the shift needs the user's call.
|
|
180
182
|
|
|
@@ -203,4 +205,4 @@ The orchestrator's backstop is the stall watchdog: three consecutive turns with
|
|
|
203
205
|
|
|
204
206
|
`complete_goal` submits a nonterminal claim, not a completion verdict. While the detached audit is pending, do not say the goal is done/complete/approved or present a final success summary. Do not wait or poll. If a response is needed, give one brief pending-status sentence. GLLA posts the concrete outcome-first summary into chat after verified approval and durable archive, without needing another user prompt. Do not duplicate that summary. A rejected claim remains unfinished and repair work continues.
|
|
205
207
|
|
|
206
|
-
The agent-written completionSummary is user-facing evidence, not a step log: open with the outcome in plain words, then one verifiable result per label with its proof inline (test counts, file paths, commit refs) — never raw run stats. Keep each label value to one
|
|
208
|
+
The agent-written completionSummary is user-facing evidence, not a step log: open with the outcome in plain words, then one verifiable result per label with its proof inline (test counts, file paths, commit refs) — never raw run stats. Keep each label value to one or two short clauses so it survives the renderer's clause-boundary clipping (rich sections budget ~200 chars per value); cite human-readable proof (counts, versions, repo-relative paths, PR numbers) and never machine paths (/var/tmp logs, tarballs — those live in the archive only). Cut values at clause boundaries, never mid-word; write Next: as one concrete follow-up action or none (audit-self-referential lines like "verdict decides" are stripped at render); pass what was deliberately left out as the complete_goal leftOut parameter — it renders as the closing bullet, and is omitted when absent.
|