pi-goal-list-loop-audit 0.38.46 → 0.38.48
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 +22 -0
- package/INSTALL.md +9 -9
- package/README.md +14 -14
- package/docs/INDEX.md +25 -27
- package/extensions/goal-continuation.ts +14 -9
- package/extensions/goal-loop-core.ts +14 -0
- package/extensions/goal-loop.ts +2 -9
- package/extensions/loops/goal-list-queue.ts +4 -8
- package/extensions/loops/goal-tools.ts +146 -41
- package/extensions/prompt-layers.ts +145 -0
- package/extensions/task-batch.ts +156 -0
- package/package.json +1 -1
- package/prompts/goal-loop-continuation.md +6 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.38.48 — atomic task batches + pending-task completion gate (2026-09-11)
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **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.
|
|
8
|
+
- **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).
|
|
9
|
+
- **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.
|
|
10
|
+
|
|
11
|
+
### Tests
|
|
12
|
+
|
|
13
|
+
- `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.
|
|
14
|
+
|
|
15
|
+
## 0.38.47 — layered prompts: skeleton plus on-demand detail (2026-09-11)
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- **Prompt layers:** every `prompts/*.md` file is now a static skeleton plus on-demand detail blocks (`<!-- glla-layer: detail <id> -->`). The continuation template carries three conditional details — `auditor-disapproval` (live disapproval), `survey-pivot` (survey objective), `session-restart` (resync/recovery) — loaded only when their trigger is already known at render time; loop, draft, and plan prompts are skeleton-only (per-iteration essential or already file-granular on-demand). Static layers resolve before dynamic substitution, in file order every turn.
|
|
20
|
+
- **Measured saving:** clean-scenario full continuation render drops 24,347 to 21,801 chars (~10%, est. tokens 6,087 to 5,451); firing predicates re-add exactly their own bodies.
|
|
21
|
+
- **Loud failure:** a missing prompt file, unclosed/duplicate block, or unknown detail id throws naming the file — the `[template-not-found]`, inline mini-prompt, and `[DRAFTING] Clarify` silent fallbacks are gone.
|
|
22
|
+
- **Mid-turn contingencies stay put:** subagent death, provider errors, detached commits, stalls, and task workflow remain in the skeleton (their trigger cannot be known before the turn; no mid-turn fetch exists).
|
|
23
|
+
- **Pins:** `tests/prompt-layers.test.ts` (19) — full-assembly render-diff for all 7 files, exact-delta proofs, loud-missing, order determinism; `tests/context-growth-measurement.test.ts` and `tests/context-checkpoint.test.ts` re-baselined deliberately (deltas verified byte-exact against a pre-change worktree).
|
|
24
|
+
|
|
3
25
|
## 0.38.46 — rich Antigravity-style terminal summaries (2026-09-11)
|
|
4
26
|
|
|
5
27
|
### 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.46`).
|
|
35
35
|
When the npm registry is ahead, it also nudges:
|
|
36
36
|
|
|
37
37
|
```text
|
|
38
|
-
glla: … · v0.38.
|
|
38
|
+
glla: … · v0.38.45 · update v0.38.46 available
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
The nudge comes from a daily sidecar check (`.pi-glla/update-check.json`),
|
|
@@ -45,7 +45,7 @@ refreshed on command contact and never blocking a turn. To update:
|
|
|
45
45
|
pi install npm:pi-goal-list-loop-audit@latest
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
-
Then `/reload` every open pi session
|
|
48
|
+
Then `/reload` every open pi session: a session keeps running the
|
|
49
49
|
version it loaded with until reloaded. `/glla version` says whether the
|
|
50
50
|
current session is stale and repeats the update command.
|
|
51
51
|
|
|
@@ -58,7 +58,7 @@ and confirmation UX:
|
|
|
58
58
|
pi install npm:@juicesharp/rpiv-ask-user-question
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
-
For best automation and quality, add the **pinned parallel-orchestration companion** (`pi-subagents` 0.62.0)
|
|
61
|
+
For best automation and quality, add the **pinned parallel-orchestration companion** (`pi-subagents` 0.62.0): GLLA's power-max choice for `runs.all` fan-out, `runs.lanes` worker→review→fix chains, structured verification, worktree isolation, and durable recovery:
|
|
62
62
|
|
|
63
63
|
```bash
|
|
64
64
|
pi install npm:pi-subagents@0.62.0
|
|
@@ -260,9 +260,9 @@ supervisor, or let it own the session instead of GLLA.
|
|
|
260
260
|
|
|
261
261
|
## Further reading
|
|
262
262
|
|
|
263
|
-
- [`README.md`](README.md)
|
|
263
|
+
- [`README.md`](README.md): product overview, first-use guide, commands,
|
|
264
264
|
companion policy, autonomy model, recovery, and maintainer map;
|
|
265
|
-
- [`docs/DESIGN.md`](docs/DESIGN.md)
|
|
266
|
-
- [`docs/INDEX.md`](docs/INDEX.md)
|
|
267
|
-
- [`docs/RELEASING.md`](docs/RELEASING.md)
|
|
268
|
-
- [`CHANGELOG.md`](CHANGELOG.md)
|
|
265
|
+
- [`docs/DESIGN.md`](docs/DESIGN.md): architecture and design decisions;
|
|
266
|
+
- [`docs/INDEX.md`](docs/INDEX.md): shipped and repository-only documentation;
|
|
267
|
+
- [`docs/RELEASING.md`](docs/RELEASING.md): release process;
|
|
268
|
+
- [`CHANGELOG.md`](CHANGELOG.md): version history.
|
package/README.md
CHANGED
|
@@ -94,7 +94,7 @@ Done when:
|
|
|
94
94
|
```
|
|
95
95
|
|
|
96
96
|
The contract is the important part. Replace the example with the result you
|
|
97
|
-
actually want and checks that another person
|
|
97
|
+
actually want and checks that another person (or another agent) could inspect.
|
|
98
98
|
|
|
99
99
|
For an objective that needs shaping, start with bare `/goal` and answer the
|
|
100
100
|
interview. GLLA will research the ambiguity, ask focused questions, and show a
|
|
@@ -132,7 +132,7 @@ forcing every problem into a loop.
|
|
|
132
132
|
| `/list` | Several outcomes or a backlog of independently verifiable items | Each item is worked and audited separately; the queue advances safely |
|
|
133
133
|
| `/loop` | Ongoing improvement with no single final item | A metric, specification, audit cadence, bound, or `/loop stop` ends the process |
|
|
134
134
|
|
|
135
|
-
### `/goal
|
|
135
|
+
### `/goal`: one outcome
|
|
136
136
|
|
|
137
137
|
```text
|
|
138
138
|
/goal # interview + Confirm
|
|
@@ -152,7 +152,7 @@ A goal is the best default for work with a finish line. If the agent discovers
|
|
|
152
152
|
that the objective is too large, it can propose a bounded task plan instead of
|
|
153
153
|
quietly inventing an unbounded backlog.
|
|
154
154
|
|
|
155
|
-
### `/list
|
|
155
|
+
### `/list`: a durable work pool
|
|
156
156
|
|
|
157
157
|
```text
|
|
158
158
|
/list "fix the cache. Done when: tests pass"
|
|
@@ -179,11 +179,11 @@ bounded bootstrap turn containing `propose_task_list`. Confirm the redraft;
|
|
|
179
179
|
automatic repeats are fenced. Use `/list resume` or `/glla resume` for an
|
|
180
180
|
intentional retry of a waiting/restored queue, and `/list next` when you
|
|
181
181
|
intentionally want to skip or choose another queued item. `/list start` is
|
|
182
|
-
also explicit: it activates the queued head, or
|
|
182
|
+
also explicit: it activates the queued head, or, when the queue is empty, uses
|
|
183
183
|
one clear recent user request as a seed for the normal Confirm-gated list
|
|
184
184
|
drafting flow. Ambiguous context is never queued automatically.
|
|
185
185
|
|
|
186
|
-
### `/loop
|
|
186
|
+
### `/loop`: an improvement process
|
|
187
187
|
|
|
188
188
|
```text
|
|
189
189
|
/loop # interview + Confirm
|
|
@@ -274,13 +274,13 @@ GLLA is the supervisor. These companions add capabilities around it:
|
|
|
274
274
|
|
|
275
275
|
### Recommended for almost everyone
|
|
276
276
|
|
|
277
|
-
- **`@juicesharp/rpiv-ask-user-question
|
|
277
|
+
- **`@juicesharp/rpiv-ask-user-question`**: structured questions, multi-select,
|
|
278
278
|
previews, and Confirm dialogs for drafting and decisions. GLLA has a prose
|
|
279
279
|
fallback, but this is the intended UX.
|
|
280
280
|
|
|
281
|
-
### Recommended for power
|
|
281
|
+
### Recommended for power: parallel orchestration (`pi-subagents`)
|
|
282
282
|
|
|
283
|
-
- **`pi-subagents` 0.62.0 (pinned)
|
|
283
|
+
- **`pi-subagents` 0.62.0 (pinned): the power-max choice for GLLA.** Use it
|
|
284
284
|
when you want the best automation and quality: `runs.all` parallel fan-out,
|
|
285
285
|
`runs.lanes` worker→review→fix chains, `outputSchema` + `acceptance` structured
|
|
286
286
|
verification, `runs.host` gated shell, worktree isolation, model routing
|
|
@@ -295,7 +295,7 @@ GLLA is the supervisor. These companions add capabilities around it:
|
|
|
295
295
|
|
|
296
296
|
The main pi session remains the owner of the goal/list/loop; subagents are
|
|
297
297
|
workers and cannot silently replace the parent's objective. A short or mostly
|
|
298
|
-
sequential goal can still run cleanly without workers
|
|
298
|
+
sequential goal can still run cleanly without workers; install when
|
|
299
299
|
parallelism will pay for its coordination and model usage.
|
|
300
300
|
|
|
301
301
|
Display coexistence: pi-subagents renders its own inline run panels +
|
|
@@ -304,7 +304,7 @@ GLLA is the supervisor. These companions add capabilities around it:
|
|
|
304
304
|
swapping order, set pi-subagents `inlineToolDisplay: "summary"` (one
|
|
305
305
|
stable row per run) and pin `fleetViewPlacement`; GLLA's own richness is
|
|
306
306
|
the `subagentDisplayRichness` setting (`/glla` → Subagents): `rich`
|
|
307
|
-
(default
|
|
307
|
+
(default: worker rows + task linkage), `compact` (count line), `quiet`
|
|
308
308
|
(hung/aborting workers only). Upstream triple-render report:
|
|
309
309
|
nicobailon/pi-subagents#1931.
|
|
310
310
|
|
|
@@ -319,17 +319,17 @@ recommendation in the same session. Existing Tintin-era agent files are
|
|
|
319
319
|
cleaned only when GLLA's management marker proves that GLLA owns them; old
|
|
320
320
|
settings are not silently remapped to a different role. Do not stack
|
|
321
321
|
`@quintinshaw/pi-dynamic-workflows` as a competing orchestrator alongside
|
|
322
|
-
GLLA + `pi-subagents` in the same session
|
|
322
|
+
GLLA + `pi-subagents` in the same session: duplicate tools and competing
|
|
323
323
|
orchestration events create ambiguous ownership. Use its quality helpers
|
|
324
324
|
(`verify`/`judgePanel`/`loopUntilDry`) only as isolated complements if needed.
|
|
325
325
|
|
|
326
326
|
### Useful, but optional
|
|
327
327
|
|
|
328
|
-
- **`@pi-unipi/notify
|
|
328
|
+
- **`@pi-unipi/notify`**: Telegram, Gotify, or ntfy delivery when you need
|
|
329
329
|
alerts away from the desktop. GLLA's local notifications work without it;
|
|
330
330
|
when no command is configured, it auto-detects `notify-send`/`osascript`;
|
|
331
331
|
`notify=off` silences notifications.
|
|
332
|
-
- **`pi-chrome
|
|
332
|
+
- **`pi-chrome`**: logged-in browser research and interaction when a goal needs
|
|
333
333
|
a real web session. It is not required for repository-only work.
|
|
334
334
|
|
|
335
335
|
### What not to combine with GLLA
|
|
@@ -501,4 +501,4 @@ repository material, not first-use package content.
|
|
|
501
501
|
|
|
502
502
|
## License
|
|
503
503
|
|
|
504
|
-
GNU Affero General Public License v3.0-only
|
|
504
|
+
GNU Affero General Public License v3.0-only; see [LICENSE](LICENSE).
|
package/docs/INDEX.md
CHANGED
|
@@ -1,16 +1,14 @@
|
|
|
1
|
-
# docs/
|
|
1
|
+
# docs/ index
|
|
2
2
|
|
|
3
3
|
Ordered by reading path, not alphabetically.
|
|
4
4
|
|
|
5
5
|
## Active focus (recent work, durable artifacts)
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
Policy contracts and recent changes live in the `audit/` directory of the
|
|
8
|
+
**repository checkout**; that directory is not shipped in the npm tarball
|
|
9
|
+
(see "Repository-only material" below). The shipped entry points are:
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
- `../CHANGELOG.md` — user-facing changelog; an `Unreleased` section may lead
|
|
11
|
+
- `../CHANGELOG.md`: user-facing changelog; an `Unreleased` section may lead
|
|
14
12
|
the file, followed by the current released version. v0.35.5 adopted the six-label completion
|
|
15
13
|
recap; v0.35.6 added typed-boundary regression pins; v0.35.7 added
|
|
16
14
|
deterministic fast-fail pre-audits, zero-pause autonomous execution, and
|
|
@@ -18,7 +16,7 @@ For shipped docs, the relevant entry points are:
|
|
|
18
16
|
failback; v0.35.9 hardened cross-version npm tarball checks; v0.35.10
|
|
19
17
|
handles multi-entry npm dry-run reports; v0.35.11 accepts both npm report
|
|
20
18
|
shapes; v0.35.12 supports npm 12's keyed pack reports; v0.35.13 fixes stale-API recovery loops.
|
|
21
|
-
v0.35.14–v0.38.
|
|
19
|
+
v0.35.14–v0.38.48 continue through the supervisor freeze (`/glla pause`),
|
|
22
20
|
load hold, auditor picker parity, Windows launch fix, zombie-watchdog
|
|
23
21
|
subagent carve-out, due-wait backstop, the `/glla agents` visibility panel,
|
|
24
22
|
durable state-root selection, blank-until-resume auditor context, frozen
|
|
@@ -28,43 +26,43 @@ For shipped docs, the relevant entry points are:
|
|
|
28
26
|
packed-artifact release verification, and the 2026-09-07 display/lifecycle/
|
|
29
27
|
settings audit pass (abort-latch send guards, ownership compare-and-swap,
|
|
30
28
|
auditor inherit/clear parity) and the v0.38.25 post-objective summary
|
|
31
|
-
(canonical approval render, persist + replay, audit-goal counts line), the v0.38.27 selective port of PRs #45/#46 (`/loop pause` soft-hold, widget subtask count), the v0.38.28 provenance-row closure fix, and the v0.38.29 compact active-card recovery/judgment projection
|
|
32
|
-
- `../README.md
|
|
29
|
+
(canonical approval render, persist + replay, audit-goal counts line), the v0.38.27 selective port of PRs #45/#46 (`/loop pause` soft-hold, widget subtask count), the v0.38.28 provenance-row closure fix, and the v0.38.29 compact active-card recovery/judgment projection; see CHANGELOG.md for the full trail.
|
|
30
|
+
- `../README.md`: what the plugin is, install, quickstart, and the
|
|
33
31
|
architectural guarantee (drafting + confirm + detached auditor).
|
|
34
|
-
- `../INSTALL.md
|
|
32
|
+
- `../INSTALL.md`: source install / local development setup; the recommended
|
|
35
33
|
companion plugins and the `auditor reads / writes are path-checked`
|
|
36
34
|
note.
|
|
37
35
|
|
|
38
36
|
## Entry points
|
|
39
|
-
- `../README.md
|
|
40
|
-
- `SETTINGS.md
|
|
41
|
-
- `../INSTALL.md
|
|
42
|
-
- `../CHANGELOG.md
|
|
37
|
+
- `../README.md`: what the plugin is, install, quickstart
|
|
38
|
+
- `SETTINGS.md`: canonical settings reference (files, precedence, every key)
|
|
39
|
+
- `../INSTALL.md`: source install / local development setup
|
|
40
|
+
- `../CHANGELOG.md`: user-facing changelog; the first versioned heading is
|
|
43
41
|
the current released package version (use `/glla version` to compare with
|
|
44
42
|
the registry); post-release work may appear in `Unreleased` above it.
|
|
45
43
|
|
|
46
44
|
## Architecture
|
|
47
|
-
- `DESIGN.md
|
|
48
|
-
- `DESIGN-long-running-supervision.md
|
|
49
|
-
- `GLLA-POSITIONING-AND-DECOMPOSITION-2026-08-08.md
|
|
45
|
+
- `DESIGN.md`: plugin design (types, state, extension lifecycle)
|
|
46
|
+
- `DESIGN-long-running-supervision.md`: v0.36.0 event/progress-driven supervision, aggressive recovery, terminal recaps, and future decision checklist
|
|
47
|
+
- `GLLA-POSITIONING-AND-DECOMPOSITION-2026-08-08.md`: ecosystem
|
|
50
48
|
positioning, competitor review, and the goal.ts decomposition plan
|
|
51
|
-
(the current strategic doc
|
|
49
|
+
(the current strategic doc; read this before touching
|
|
52
50
|
`extensions/loops/goal.ts`)
|
|
53
|
-
- `VISION-ASSIST.md
|
|
54
|
-
- `RELEASING.md
|
|
51
|
+
- `VISION-ASSIST.md`: vision-assist plugin notes
|
|
52
|
+
- `RELEASING.md`: how to publish to npm
|
|
55
53
|
|
|
56
54
|
## Supporting material
|
|
57
|
-
- `../prompts
|
|
58
|
-
- `../schemas
|
|
59
|
-
- `../examples
|
|
60
|
-
- `../CHANGELOG.md
|
|
61
|
-
- `/glla bug
|
|
55
|
+
- `../prompts/`: goal/loop drafting prompt templates
|
|
56
|
+
- `../schemas/`: goal state JSON schema
|
|
57
|
+
- `../examples/`: example objective files
|
|
58
|
+
- `../CHANGELOG.md`: user-facing changelog (unreleased at top)
|
|
59
|
+
- `/glla bug`: `extensions/goal-commands.ts:cmdGllaBug` captures failure context to `<stateDir>/bugs/<ts>-<id>.md` without touching `active.jsonl`/`goals/*.md` (see `tests/glla-bug-capture.test.ts`)
|
|
62
60
|
|
|
63
61
|
## Repository-only material
|
|
64
62
|
The audit history and competitor research live in `audit/` and `.research/`
|
|
65
63
|
for contributors, but are intentionally not included in the npm tarball.
|
|
66
64
|
|
|
67
65
|
## Research material
|
|
68
|
-
`.research
|
|
66
|
+
`.research/`: competitor plugin sources pulled from npm tarballs for study
|
|
69
67
|
(gitignored, local only). Re-pull with `cd .research && npm pack <pkg> &&
|
|
70
68
|
tar xzf <tgz>`; see the positioning doc's appendix for the package list.
|
|
@@ -56,6 +56,7 @@ import {
|
|
|
56
56
|
type ObjectiveRepairTarget,
|
|
57
57
|
} from "./goal-loop-core.js";
|
|
58
58
|
import { auditorSurfaceSuppressed } from "./loops/goal-auditor-surface.js";
|
|
59
|
+
import { assemblePrompt } from "./prompt-layers.js";
|
|
59
60
|
import {
|
|
60
61
|
createContinuationDispatch,
|
|
61
62
|
transitionDispatch,
|
|
@@ -1235,7 +1236,7 @@ export function buildContinuationContent(goal: Goal, opts: { resync?: string; fi
|
|
|
1235
1236
|
const resync = opts.resync ?? "";
|
|
1236
1237
|
const marker = buildMarkerContent(goal.id);
|
|
1237
1238
|
const needsFull = (opts.firstSend ?? false) || needsFullContinuation(goal);
|
|
1238
|
-
if (needsFull) return { content: resync + continuationPrompt(goal), kind: resync ? "full+resync" : "full" };
|
|
1239
|
+
if (needsFull) return { content: resync + continuationPrompt(goal, { includeRestartDetail: Boolean(resync) || Boolean(goal.autoResumedAt) }), kind: resync ? "full+resync" : "full" };
|
|
1239
1240
|
if (resync) return { content: resync + marker, kind: "resync" };
|
|
1240
1241
|
return { content: marker, kind: "marker" };
|
|
1241
1242
|
}
|
|
@@ -1493,7 +1494,7 @@ export function buildPostCompactResync(briefExcerpt?: string): string {
|
|
|
1493
1494
|
return lines.join("\n") + "\n\n";
|
|
1494
1495
|
}
|
|
1495
1496
|
|
|
1496
|
-
export function continuationPrompt(goal: Goal): string {
|
|
1497
|
+
export function continuationPrompt(goal: Goal, opts: { includeRestartDetail?: boolean } = {}): string {
|
|
1497
1498
|
// Read the .md file as the template, then substitute {{tokens}}.
|
|
1498
1499
|
// For v0.1.0 we inline-substitute so we don't need fs at runtime.
|
|
1499
1500
|
const next = findNextPendingTask(goal.taskList?.tasks ?? []);
|
|
@@ -1503,13 +1504,17 @@ export function continuationPrompt(goal: Goal): string {
|
|
|
1503
1504
|
const taskSummary = goal.taskList?.tasks.length
|
|
1504
1505
|
? buildTaskSummary(goal.taskList.tasks)
|
|
1505
1506
|
: "(no task list)";
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1507
|
+
// Layered assembly: skeleton always; details only when their trigger is
|
|
1508
|
+
// already known at render time. Static layers resolve before dynamic
|
|
1509
|
+
// ${...} substitution, in file order every turn. Mid-turn contingencies
|
|
1510
|
+
// (subagent death, provider errors, detached commits, stalls, task
|
|
1511
|
+
// workflow) stay in the skeleton: their trigger cannot be known before
|
|
1512
|
+
// the turn, and there is no mid-turn detail fetch.
|
|
1513
|
+
const detailIds = new Set<string>();
|
|
1514
|
+
if (liveDisapproval(goal.auditHistory ?? [])) detailIds.add("auditor-disapproval");
|
|
1515
|
+
if (isFullAuditObjective(goal.objective)) detailIds.add("survey-pivot");
|
|
1516
|
+
if (opts.includeRestartDetail || goal.autoResumedAt) detailIds.add("session-restart");
|
|
1517
|
+
const tmpl = assemblePrompt("goal-loop-continuation.md", detailIds);
|
|
1513
1518
|
// v0.25.0 (contract items 22/28): conditional directives — aggressiveMode
|
|
1514
1519
|
// TODOs from the audit cap, and the full-audit fan-out directive when the
|
|
1515
1520
|
// objective reads as a survey pivot. The canonical judgment policy and
|
|
@@ -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 {
|
package/extensions/goal-loop.ts
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
supervisorPaused,
|
|
27
27
|
} from "./goal-loop-core.js";
|
|
28
28
|
import { BACKOFF_IDLE_RETRY_MS, MEASURE_TIMEOUT_MS } from "./goal-loop-backoff.js";
|
|
29
|
+
import { loadPromptWhole } from "./prompt-layers.js";
|
|
29
30
|
import {
|
|
30
31
|
REPETITION,
|
|
31
32
|
continueVariant,
|
|
@@ -285,15 +286,7 @@ function loopPrompt(loop: LoopState, regressionNote: string, strategyNote: strin
|
|
|
285
286
|
// v0.23.0: metricless loops get their own prompt — no metric section,
|
|
286
287
|
// anti-doorknob rules instead of anti-gaming rules.
|
|
287
288
|
const metricless = !loop.measureCmd;
|
|
288
|
-
const
|
|
289
|
-
let tmpl: string;
|
|
290
|
-
try {
|
|
291
|
-
tmpl = fs.readFileSync(tmplPath, "utf-8");
|
|
292
|
-
} catch {
|
|
293
|
-
tmpl = metricless
|
|
294
|
-
? `[LOOP ITERATION ${loop.iteration + 1}] Target: ${loop.target}. Metricless spec loop — make ONE real, inspectable change advancing the target. No cosmetic churn. ${variantNote} ${interventionNote}`
|
|
295
|
-
: `[LOOP ITERATION ${loop.iteration + 1}] Target: ${loop.target}. Measure: ${loop.measureCmd} (${loop.direction}). Make ONE small change to improve the metric. ${interventionNote}`;
|
|
296
|
-
}
|
|
289
|
+
const tmpl = loadPromptWhole(metricless ? "goal-loop-forever-metricless.md" : "goal-loop-forever.md");
|
|
297
290
|
return tmpl
|
|
298
291
|
.replace(/\$\{ITERATION\}/g, String(loop.iteration + 1))
|
|
299
292
|
.replace(/\$\{TARGET\}/g, loop.target)
|
|
@@ -26,6 +26,7 @@ import { Type } from "typebox";
|
|
|
26
26
|
// (positioning doc invariant #2). Property reads on the imported binding are
|
|
27
27
|
// fine; wholesale replacement goes through replaceState().
|
|
28
28
|
import { state, replaceState, persistStateLine } from "../goal-state.js";
|
|
29
|
+
import { loadPromptWhole } from "../prompt-layers.js";
|
|
29
30
|
|
|
30
31
|
import {
|
|
31
32
|
type Goal,
|
|
@@ -863,11 +864,9 @@ async function startDrafting(ctx: ExtensionContext, target: "goal" | "list" | "l
|
|
|
863
864
|
: target === "loop"
|
|
864
865
|
? `${label}: a loop target needs a metric and a direction — the agent will help you design them first (nothing activates until you confirm). Skip the interview entirely: /loop start "<target>" (bare = infinite metricless) or /loop start "<target>" measure="<cmd>" direction=min|max [window=5] [max=50] [time=h] [tokens=n] [branch=1].`
|
|
865
866
|
: `${label}: the objective has no "Done when:" clause — the agent will grill you about it first (nothing activates until you confirm). Skip the interview entirely: /goal start <objective>.`);
|
|
866
|
-
|
|
867
|
-
let tmpl
|
|
868
|
-
|
|
869
|
-
tmpl = fs.readFileSync(tmplPath, "utf-8");
|
|
870
|
-
if (target === "list") {
|
|
867
|
+
// Layered-prompt contract: a missing draft file fails loudly — never render lean.
|
|
868
|
+
let tmpl = loadPromptWhole(file);
|
|
869
|
+
if (target === "list") {
|
|
871
870
|
tmpl = tmpl.replace(
|
|
872
871
|
"[GOAL DRAFTING]",
|
|
873
872
|
"[LIST DRAFTING — the confirmed item goes into the /list LIST, it does not activate immediately. " +
|
|
@@ -880,9 +879,6 @@ async function startDrafting(ctx: ExtensionContext, target: "goal" | "list" | "l
|
|
|
880
879
|
"is still a SHORT task — never an aggregate wrapper ('land all N findings' with a '≥N commits' contract is the " +
|
|
881
880
|
"canonical anti-pattern: the auto-committer squashes, the count fails, the auditor disapproves finished work).]",
|
|
882
881
|
);
|
|
883
|
-
}
|
|
884
|
-
} catch {
|
|
885
|
-
tmpl = `[DRAFTING] Clarify the user's ${target}, then call ${tool}.`;
|
|
886
882
|
}
|
|
887
883
|
// v0.14.0: the LLM grills (its strength — v0.13.0's canned questionnaire
|
|
888
884
|
// accepted non-answers), the plugin enforces the floor: propose_goal_draft
|
|
@@ -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,25 +2211,25 @@ 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
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
}
|
|
2174
|
-
|
|
2175
|
-
}
|
|
2176
|
-
}
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
return { content: [{ type: "text", text: `Task ${p.id} marked complete.` }], details: {} };
|
|
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 unchanged (pinned by tests).
|
|
2217
|
+
const t = findTask(tl, p.id);
|
|
2218
|
+
if (t && t.status !== "complete") {
|
|
2219
|
+
const checkRes = await verifyTaskMilestone(ctx, t.verificationContract, signal);
|
|
2220
|
+
if (checkRes) {
|
|
2221
|
+
return {
|
|
2222
|
+
content: [{
|
|
2223
|
+
type: "text",
|
|
2224
|
+
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.`,
|
|
2225
|
+
}],
|
|
2226
|
+
details: {},
|
|
2227
|
+
};
|
|
2228
|
+
}
|
|
2229
|
+
if (!updateGoal({ taskList: withTaskStatus(tl, p.id, "complete") }, ctx)) {
|
|
2230
|
+
return { content: [{ type: "text", text: `Task ${p.id} could not be marked complete — the persist failed and no state changed. Retry.` }], details: {} };
|
|
2180
2231
|
}
|
|
2181
|
-
|
|
2232
|
+
return { content: [{ type: "text", text: `Task ${p.id} marked complete.` }], details: {} };
|
|
2182
2233
|
}
|
|
2183
2234
|
return { content: [{ type: "text", text: `Task ${p.id} not found.` }], details: {} };
|
|
2184
2235
|
},
|
|
@@ -2202,32 +2253,86 @@ function registerAgentTools(pi: any): void {
|
|
|
2202
2253
|
return { content: [{ type: "text", text: "No task list in this goal." }], details: {} };
|
|
2203
2254
|
}
|
|
2204
2255
|
const tl = state.goal.taskList;
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
if (
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
};
|
|
2219
|
-
}
|
|
2256
|
+
// v0.38.48: copy-swap (see complete_task). Message strings unchanged.
|
|
2257
|
+
const t = findTask(tl, p.id);
|
|
2258
|
+
if (t) {
|
|
2259
|
+
if (p.status === "complete") {
|
|
2260
|
+
const checkRes = await verifyTaskMilestone(ctx, t.verificationContract, signal);
|
|
2261
|
+
if (checkRes) {
|
|
2262
|
+
return {
|
|
2263
|
+
content: [{
|
|
2264
|
+
type: "text",
|
|
2265
|
+
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.`,
|
|
2266
|
+
}],
|
|
2267
|
+
details: {},
|
|
2268
|
+
};
|
|
2220
2269
|
}
|
|
2221
|
-
t.status = p.status;
|
|
2222
|
-
updateGoal({ taskList: tl }, ctx);
|
|
2223
|
-
return { content: [{ type: "text", text: `Task ${p.id} → ${p.status}` }], details: {} };
|
|
2224
2270
|
}
|
|
2225
|
-
if (
|
|
2271
|
+
if (!updateGoal({ taskList: withTaskStatus(tl, p.id, p.status) }, ctx)) {
|
|
2272
|
+
return { content: [{ type: "text", text: `Task ${p.id} could not move to ${p.status} — the persist failed and no state changed. Retry.` }], details: {} };
|
|
2273
|
+
}
|
|
2274
|
+
return { content: [{ type: "text", text: `Task ${p.id} → ${p.status}` }], details: {} };
|
|
2226
2275
|
}
|
|
2227
2276
|
return { content: [{ type: "text", text: `Task ${p.id} not found.` }], details: {} };
|
|
2228
2277
|
},
|
|
2229
2278
|
}));
|
|
2230
2279
|
|
|
2280
|
+
pi.registerTool(defineTool({
|
|
2281
|
+
name: "update_task_batch",
|
|
2282
|
+
label: "Update task batch",
|
|
2283
|
+
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.",
|
|
2284
|
+
parameters: Type.Object({
|
|
2285
|
+
updates: Type.Array(Type.Object({
|
|
2286
|
+
id: Type.String({ description: "Task id to update" }),
|
|
2287
|
+
status: Type.Union([Type.Literal("pending"), Type.Literal("in_progress"), Type.Literal("complete")]),
|
|
2288
|
+
}), { description: "Status updates to apply atomically (each task id at most once)" }),
|
|
2289
|
+
}),
|
|
2290
|
+
async execute(_id, params, signal, _onUpdate, execCtx) {
|
|
2291
|
+
const foreignBatch = foreignToolGuard(execCtx);
|
|
2292
|
+
if (foreignBatch) return { content: [{ type: "text", text: foreignBatch }], details: {} };
|
|
2293
|
+
const ctx = currentToolContext(execCtx);
|
|
2294
|
+
if (!ctx) return staleToolResult();
|
|
2295
|
+
const p = params as { updates: TaskStatusUpdate[] };
|
|
2296
|
+
if (!state.goal || !state.goal.taskList) {
|
|
2297
|
+
return { content: [{ type: "text", text: "No task list in this goal." }], details: {} };
|
|
2298
|
+
}
|
|
2299
|
+
const tl = state.goal.taskList;
|
|
2300
|
+
// Phase 1 — validate the WHOLE batch before any verification runs.
|
|
2301
|
+
const validation = validateTaskBatch(tl, p.updates);
|
|
2302
|
+
if (!validation.ok) {
|
|
2303
|
+
return { content: [{ type: "text", text: validation.error }], details: {} };
|
|
2304
|
+
}
|
|
2305
|
+
// Phase 2 — verify every milestone moving to complete. All failures
|
|
2306
|
+
// are collected so one bad milestone does not hide another; NOTHING
|
|
2307
|
+
// is applied until every verification passes (mid-batch kill here
|
|
2308
|
+
// leaves state unchanged — the copy below does not exist yet).
|
|
2309
|
+
const failures: string[] = [];
|
|
2310
|
+
for (const e of validation.entries) {
|
|
2311
|
+
if (e.status === "complete" && e.task.status !== "complete") {
|
|
2312
|
+
const checkRes = await verifyTaskMilestone(ctx, e.task.verificationContract, signal);
|
|
2313
|
+
if (checkRes) {
|
|
2314
|
+
failures.push(`Task ${e.task.id} milestone verification FAILED for command \`${checkRes.failedCommand}\` (exit code ${checkRes.exitCode}):\n\n${checkRes.output}`);
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
2318
|
+
if (failures.length > 0) {
|
|
2319
|
+
return {
|
|
2320
|
+
content: [{
|
|
2321
|
+
type: "text",
|
|
2322
|
+
text: `Task batch rejected — ${failures.length} milestone verification${failures.length === 1 ? "" : "s"} failed. No task was changed:\n\n${failures.join("\n\n")}`,
|
|
2323
|
+
}],
|
|
2324
|
+
details: {},
|
|
2325
|
+
};
|
|
2326
|
+
}
|
|
2327
|
+
// Phase 3 — copy, apply, persist ONCE.
|
|
2328
|
+
if (!updateGoal({ taskList: applyValidatedBatch(tl, validation.entries) }, ctx)) {
|
|
2329
|
+
return { content: [{ type: "text", text: "Task batch could not persist — the write failed and no state changed. Retry." }], details: {} };
|
|
2330
|
+
}
|
|
2331
|
+
const applied = validation.entries.map((e) => `${e.task.id} → ${e.status}`).join(", ");
|
|
2332
|
+
return { content: [{ type: "text", text: `Task batch applied (${validation.entries.length}): ${applied}.` }], details: {} };
|
|
2333
|
+
},
|
|
2334
|
+
}));
|
|
2335
|
+
|
|
2231
2336
|
pi.registerTool(defineTool({
|
|
2232
2337
|
name: "propose_goal_draft",
|
|
2233
2338
|
label: "Propose goal draft",
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// prompt-layers.ts — static skeleton + on-demand detail assembly for prompts/
|
|
3
|
+
// ============================================================================
|
|
4
|
+
// Every prompts/*.md file is one canonical, editor-renderable document.
|
|
5
|
+
// Detail sections are wrapped in zero-width structural markers:
|
|
6
|
+
//
|
|
7
|
+
// <!-- glla-layer: detail <id> -->
|
|
8
|
+
// ...section body, untouched...
|
|
9
|
+
// <!-- glla-layer: end -->
|
|
10
|
+
//
|
|
11
|
+
// Everything outside a detail block is the skeleton: always loaded.
|
|
12
|
+
// A detail block loads only when its phase predicate holds. Assembly order
|
|
13
|
+
// is file order, every turn — static layers resolve before dynamic
|
|
14
|
+
// ${...} substitution slots, so renders are deterministic.
|
|
15
|
+
//
|
|
16
|
+
// Loudness contract: any structural problem (missing file, unclosed block,
|
|
17
|
+
// duplicate id, end-without-start, unknown requested id) THROWS naming the
|
|
18
|
+
// file. A render never proceeds lean on a "[template-not-found]" fallback.
|
|
19
|
+
// Marker lines are structural only and are stripped from assembled output,
|
|
20
|
+
// so full assembly (every detail active) is byte-identical to the source
|
|
21
|
+
// file minus its marker lines.
|
|
22
|
+
// ============================================================================
|
|
23
|
+
|
|
24
|
+
import * as fs from "node:fs";
|
|
25
|
+
import * as path from "node:path";
|
|
26
|
+
|
|
27
|
+
const LAYER_START_RE = /^\s*<!--\s*glla-layer:\s*detail\s+([A-Za-z0-9_-]+)\s*-->\s*$/;
|
|
28
|
+
const LAYER_END_RE = /^\s*<!--\s*glla-layer:\s*end\s*-->\s*$/;
|
|
29
|
+
|
|
30
|
+
export interface PromptSegment {
|
|
31
|
+
kind: "skeleton" | "detail";
|
|
32
|
+
/** Present only for detail segments. */
|
|
33
|
+
id?: string;
|
|
34
|
+
/** Exact source lines for this segment (marker lines excluded). */
|
|
35
|
+
lines: string[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function promptFilePath(name: string): string {
|
|
39
|
+
return path.resolve(__dirname, "..", "prompts", name);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Parse one prompt source into ordered segments. Throws naming the file
|
|
43
|
+
* on any structural defect. Pure (no fs) so tests can feed fixtures. */
|
|
44
|
+
export function parsePromptLayers(source: string, name: string): PromptSegment[] {
|
|
45
|
+
const segments: PromptSegment[] = [];
|
|
46
|
+
let current: PromptSegment = { kind: "skeleton", lines: [] };
|
|
47
|
+
let openId: string | null = null;
|
|
48
|
+
const seen = new Set<string>();
|
|
49
|
+
const fail = (why: string): never => {
|
|
50
|
+
throw new Error(`[glla] prompts/${name}: ${why}`);
|
|
51
|
+
};
|
|
52
|
+
for (const line of source.split("\n")) {
|
|
53
|
+
const start = line.match(LAYER_START_RE);
|
|
54
|
+
const end = LAYER_END_RE.test(line);
|
|
55
|
+
if (start) {
|
|
56
|
+
if (openId !== null) fail(`detail "${openId}" unclosed before detail "${start[1]}" starts`);
|
|
57
|
+
const id = start[1] as string;
|
|
58
|
+
if (seen.has(id)) fail(`duplicate detail id "${id}"`);
|
|
59
|
+
seen.add(id);
|
|
60
|
+
segments.push(current);
|
|
61
|
+
current = { kind: "detail", id, lines: [] };
|
|
62
|
+
openId = id;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (end) {
|
|
66
|
+
if (openId === null) fail("glla-layer end marker without an open detail block");
|
|
67
|
+
segments.push(current);
|
|
68
|
+
current = { kind: "skeleton", lines: [] };
|
|
69
|
+
openId = null;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
current.lines.push(line);
|
|
73
|
+
}
|
|
74
|
+
if (openId !== null) fail(`detail "${openId}" unclosed at end of file`);
|
|
75
|
+
segments.push(current);
|
|
76
|
+
return segments;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Read + parse a prompt file. Throws naming the path when unreadable. */
|
|
80
|
+
export function loadPromptSegments(name: string): PromptSegment[] {
|
|
81
|
+
const file = promptFilePath(name);
|
|
82
|
+
let source: string;
|
|
83
|
+
try {
|
|
84
|
+
source = fs.readFileSync(file, "utf-8");
|
|
85
|
+
} catch (err) {
|
|
86
|
+
throw new Error(
|
|
87
|
+
`[glla] prompt file missing: prompts/${name} (${file}): ${(err as Error)?.message ?? err}. ` +
|
|
88
|
+
"Refusing to render lean — restore the file, do not proceed without it.",
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
return parsePromptLayers(source, name);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Raw whole-file read. Throws (loud) when unreadable. Use for
|
|
95
|
+
* skeleton-only phases and for render-diff baselines. */
|
|
96
|
+
export function loadPromptWhole(name: string): string {
|
|
97
|
+
const file = promptFilePath(name);
|
|
98
|
+
try {
|
|
99
|
+
return fs.readFileSync(file, "utf-8");
|
|
100
|
+
} catch (err) {
|
|
101
|
+
throw new Error(
|
|
102
|
+
`[glla] prompt file missing: prompts/${name} (${file}): ${(err as Error)?.message ?? err}. ` +
|
|
103
|
+
"Refusing to render lean — restore the file, do not proceed without it.",
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Detail ids declared by a prompt file, in file order. */
|
|
109
|
+
export function promptDetailIds(name: string): string[] {
|
|
110
|
+
const ids: string[] = [];
|
|
111
|
+
for (const seg of loadPromptSegments(name)) {
|
|
112
|
+
if (seg.kind === "detail" && seg.id) ids.push(seg.id);
|
|
113
|
+
}
|
|
114
|
+
return ids;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Assemble skeleton + the requested details in file order. Unknown ids
|
|
118
|
+
* throw. Empty `active` renders the bare skeleton. */
|
|
119
|
+
export function assemblePrompt(name: string, active: Iterable<string>): string {
|
|
120
|
+
const segments = loadPromptSegments(name);
|
|
121
|
+
const want = new Set(active);
|
|
122
|
+
const known = new Set<string>();
|
|
123
|
+
for (const seg of segments) {
|
|
124
|
+
if (seg.kind === "detail" && seg.id) known.add(seg.id);
|
|
125
|
+
}
|
|
126
|
+
for (const id of want) {
|
|
127
|
+
if (!known.has(id)) {
|
|
128
|
+
throw new Error(
|
|
129
|
+
`[glla] prompts/${name}: unknown detail id "${id}" (known: ${[...known].join(", ") || "(none)"}). ` +
|
|
130
|
+
"Refusing to render lean — fix the request, do not proceed without it.",
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const out: string[] = [];
|
|
135
|
+
for (const seg of segments) {
|
|
136
|
+
if (seg.kind === "skeleton" || (seg.id && want.has(seg.id))) out.push(...seg.lines);
|
|
137
|
+
}
|
|
138
|
+
return out.join("\n");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Full assembly: every declared detail active. Render-diff gate: this must
|
|
142
|
+
* equal the source file minus its marker lines. */
|
|
143
|
+
export function assemblePromptFull(name: string): string {
|
|
144
|
+
return assemblePrompt(name, promptDetailIds(name));
|
|
145
|
+
}
|
|
@@ -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.48",
|
|
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",
|
|
@@ -77,6 +77,7 @@ When the agent calls any of these, the orchestrator tracks the call and persists
|
|
|
77
77
|
- **Bound every long command.** Wrap test suites, builds, and dev servers in `timeout <seconds>` (e.g. `timeout 120 bun test src/lib`). An unbounded command that hangs burns an hour; a bounded one burns two minutes and tells you it hung. If a command produces no output for many minutes, treat it as hung: kill it, diagnose why, rerun bounded.
|
|
78
78
|
- **Chunk output near context-full & microcompaction.** When the conversation is heavy (long-running audit, deep debug, big rollout), prefer smaller commits, smaller tool outputs, and focused reasoning — one or two punchy paragraphs, one well-scoped tool call at a time. Don't try to fit a thousand lines of work into one reply. Spool massive stdout/diffs to disk logs if needed. glla's auto-continue fires on `stop_reason="length"` and will reschedule you; chunking is cheaper than recovering from the cap. Save large file writes for their own turns.
|
|
79
79
|
|
|
80
|
+
<!-- glla-layer: detail auditor-disapproval -->
|
|
80
81
|
## WHEN THE AUDITOR DISAPPROVES
|
|
81
82
|
|
|
82
83
|
If the orchestrator tells you the auditor disapproved, **investigate before asking the user**:
|
|
@@ -90,7 +91,9 @@ If the orchestrator tells you the auditor disapproved, **investigate before aski
|
|
|
90
91
|
"The auditor's last 3 reports all complain about saves-3 not shipping. The current objective IS saves-3, but the work shipped is menu-3 + kingdom-2 (different items). I shipped those because [reason]. The auditor is disapproving because the original objective isn't literally shipped. Three options: A. /goal tweak the objective to menu-3+kingdom-2, then /goal resume — B. Re-scope saves-3 and ship it — C. Pivot to a different item entirely."
|
|
91
92
|
|
|
92
93
|
Do NOT ask the user to choose between generic options like "/goal resume / Move on silently / Different item". Those options tell the user nothing. Always include YOUR ASSESSMENT with quoted objections and shipped evidence.
|
|
94
|
+
<!-- glla-layer: end -->
|
|
93
95
|
|
|
96
|
+
<!-- glla-layer: detail survey-pivot -->
|
|
94
97
|
## PIVOT DETECTION
|
|
95
98
|
|
|
96
99
|
When the user says "do a full audit", "survey the project", "find all problems", "mark a tasklist", or similar — the goal is a SURVEY, not a single fix. You must:
|
|
@@ -100,6 +103,7 @@ When the user says "do a full audit", "survey the project", "find all problems",
|
|
|
100
103
|
3. Use subagents to PARALLEL-survey different subsystems (game logic, UI, audio, tests, docs) — one `scout` agent per subsystem, spawned in a single message.
|
|
101
104
|
4. Don't ship a single bug fix and then ask if the user wants to continue — the user already said "do a full audit".
|
|
102
105
|
5. After the task list is confirmed, work through tasks systematically with `complete_task` / `update_task_status`.
|
|
106
|
+
<!-- glla-layer: end -->
|
|
103
107
|
|
|
104
108
|
## WHEN SUBAGENTS DIE: RESUME, DON'T RESPAWN
|
|
105
109
|
|
|
@@ -108,9 +112,11 @@ A failed subagent's SESSION survives its death (verified in pi-subagents source:
|
|
|
108
112
|
1. **Resume it immediately**: `Agent(resume: "<id>", prompt: "Wrap up now: report what you found within ~150 lines.")`. A resumed agent already HAS its research; it only needs to report. Do not infer a reset time from the error text.
|
|
109
113
|
2. **Resume failed with "not found"?** The manager forgot it (a /reload wipes the in-process registry — old IDs are unresumable). Only NOW respawn, and never the same wide brief: SPLIT into 2 narrower agents or ABSORB the remainder inline.
|
|
110
114
|
|
|
115
|
+
<!-- glla-layer: detail session-restart -->
|
|
111
116
|
## AFTER A SESSION RESTART: YOUR SUBAGENTS ARE DEAD
|
|
112
117
|
|
|
113
118
|
Subagents run IN-PROCESS — a /reload or restart kills every running one instantly and silently (no failure event, no ✗), and their IDs become unresumable. On a restored session, check your transcript for in-flight agents and relaunch them (tight briefs) or absorb their scope — do NOT sit waiting for results that can never arrive. This is why long fan-out passes belong under a glla goal/list item: the goal plane survives restarts and re-drives the fan-out through the continuation; an ad-hoc fan-out dies with the tab.
|
|
119
|
+
<!-- glla-layer: end -->
|
|
114
120
|
|
|
115
121
|
## WHEN SUBAGENTS HIT PROVIDER/RUNTIME ERRORS
|
|
116
122
|
|