Merge M7: operator ergonomics + release 0.0.6

Closes #24
This commit is contained in:
2026-09-02 22:14:18 -05:00
5 changed files with 150 additions and 12 deletions
+54
View File
@@ -221,4 +221,58 @@ Release model and safe updates verified by drills. `main` merged with M3 and tag
Adapter seam verified; harness boundary is now additive by construction. `main` merged with M4 and tagged `adapter-seam-v1`. Adapter seam verified; harness boundary is now additive by construction. `main` merged with M4 and tagged `adapter-seam-v1`.
---
## Phase 9: Workspaces + capability envelope (M5)
### Entry 9.1 — before
- Timestamp: 2026-09-03
- Intended action: Optional task workspace (absent / ":run" ephemeral / named persistent) and capabilities.tools allowlist (pi built-ins); runner plumbing via MOSAIC_WORKSPACE/MOSAIC_TOOLS; pi adapter maps to cwd + --tools; mock adapter logs delivered MOSAIC_* vars for deterministic assertions (Gitea #20, #21).
- Reason: Agents that only answer text cannot do work; the workspace+tools pair is the smallest real capability step, bounded by the container.
- Expected result: plumbing asserted via run-record stderr; live pi writes a host-visible file.
### Entry 9.2 — after
- Timestamp: 2026-09-03
- Commands run: build; mock plumbing run; validate negatives; full task suite; live workspace demo.
- Observed result: task suite 32/32; MOSAIC_WORKSPACE/MOSAIC_TOOLS asserted in run record; dataRoot/workspaces/<name> created host-side; live pi used bash to write proof.txt into the demo workspace (host-visible).
- Failure or correction: (1) batch edit dropped SUPPORTED_TOOLS const (runtime ReferenceError, exit 1 instead of 2) — restored; (2) mock env dump used `export` which this dash prints as `export K='v'` — switched to `env`; (3) selftest fed a mismatching mock response to the plain-task case — test bug, fixed.
## Phase 10: Named sessions (M6)
### Entry 10.1 — before
- Timestamp: 2026-09-03
- Intended action: Optional task.session name -> persistent session dir dataRoot/sessions/<name> via pi --session-dir; resume most recent with -c when present; isolation per name; teach/recall demo fixtures (Gitea #22, #23).
- Reason: L1 persistence is the prerequisite for any multi-step agent work.
- Expected result: session dir populated after first run; second run recalls taught context.
### Entry 10.2 — after
- Timestamp: 2026-09-03
- Commands run: build; mock plumbing run; live teach/recall E2E; task suite.
- Observed result: task suite 32/32; teach run replied REMEMBERED and session JSONL persisted host-side; recall run resumed (-c) and answered exactly 'mosaico'; single continued session file (no duplicate sessions).
- Failure or correction: none. Design note: ephemeral (--no-session) remains the default when no session is declared.
## Phase 11: Operator ergonomics (M7)
### Entry 11.1 — before
- Timestamp: 2026-09-03
- Intended action: mosaic-task.mjs show <runId> (full record + snapshots + artifacts, traversal-safe), list with workspace/session columns, RELEASE -> 0.0.6, package + health-gated activate, docs (Gitea #24).
- Reason: Run records are only as valuable as they are inspectable; release activation closes the loop on container-content changes.
- Expected result: show works for real/missing/traversal ids; suites green; 0.0.6 active.
### Entry 11.2 — after
- Timestamp: 2026-09-03
- Commands run: build; show on real/missing/traversal ids; full sweep; release package + activate.
- Observed result: config 24/24, task 32/32, release 14/14, verify PASS; 0.0.6 packaged and activated via exact-marker health gate.
- Failure or correction: showRun initially rejected valid run ids (lowercase-only regex vs uppercase timestamp) and crashed on missing ids (uncaught readdir) — both fixed and covered.
## Autonomous run result
M5 tagged `workspace-capabilities-v1`, M6 tagged `sessions-v1`, M7 tagged `operator-ergonomics-v1`; release 0.0.6 active. Tracker: docs/plans/2026-09-03_autonomous-run.md.
+25
View File
@@ -119,6 +119,31 @@ adapters/<name>/adapter.sh env in: MOSAIC_SYSTEM_PROMPT_FILE, MOSAIC_REQUEST,
See `adapters/README.md` for the full contract. See `adapters/README.md` for the full contract.
## Workspaces, capabilities, sessions (M5/M6)
Optional task fields extend what an agent can do — all defaulting to the previous behavior:
```json
{
"workspace": "demo", // ":run" ephemeral, or persistent dataRoot/workspaces/<name>
"capabilities": { "tools": ["bash", "read"] }, // pi tool allowlist; absent = no tools
"session": "demo" // persistent session at dataRoot/sessions/<name>
}
```
- The adapter runs inside the workspace; files it writes are host-visible (`dataRoot/workspaces/<name>`).
- Sessions persist via pi's documented `--session-dir`; a follow-up run in the same session resumes the conversation (`-c`) and can recall prior context. Distinct names never share state. Ephemeral (`--no-session`) remains the default when no session is declared.
- Selection authority: config for adapter/provider/model; the task file for workspace/capabilities/session.
Inspect anything:
```bash
node scripts/mosaic-task.mjs list # runs with task/workspace/session columns
node scripts/mosaic-task.mjs show <runId> # full record + snapshots + artifacts
```
Demo fixtures: `tasks/workspace-demo.json`, `tasks/session-demo-1.json` + `tasks/session-demo-2.json`.
See `docs/plans/2026-09-02_atomic-mosaic-foundation.md` for the full plan. See `docs/plans/2026-09-02_atomic-mosaic-foundation.md` for the full plan.
Inside the container: Inside the container:
+1 -1
View File
@@ -1 +1 @@
0.0.5 0.0.6
+12 -9
View File
@@ -13,8 +13,8 @@ Advance the Mosaic Stack rebuild several verified layers in one batch, focused o
|---|---|---| |---|---|---|
| M5 | Task workspaces + capability envelope (tools allowlist) | DONE | | M5 | Task workspaces + capability envelope (tools allowlist) | DONE |
| M6 | Named sessions — persistence and resume (L1) | DONE | | M6 | Named sessions — persistence and resume (L1) | DONE |
| M7 | Operator ergonomics: run inspection commands | DONE (partial by design) | | M7 | Operator ergonomics: run inspection commands | DONE |
| — | Release 0.0.6 packaged + health-gated activated | DONE | | — | Releases 0.0.5+0.0.6 packaged; 0.0.6 health-gated activated | DONE |
Explicitly deferred (do not mistake for forgotten): Explicitly deferred (do not mistake for forgotten):
- Claude/Codex/OpenCode adapters (owner: focus on Pi for now) - Claude/Codex/OpenCode adapters (owner: focus on Pi for now)
@@ -60,14 +60,17 @@ Explicitly deferred (do not mistake for forgotten):
6. Inspect any run: `node scripts/mosaic-task.mjs show <runId>` 6. Inspect any run: `node scripts/mosaic-task.mjs show <runId>`
7. Gitea: milestones M5/M6/M7 closed; issues referenced by merge commits 7. Gitea: milestones M5/M6/M7 closed; issues referenced by merge commits
## Results (to be filled at end of run) ## Results
- M5: pending - M5 merged on `main` (merge commit `ddb1554`), tagged `workspace-capabilities-v1`
- M6: pending - M6 merged on `main` (merge commit `4e2a413`), tagged `sessions-v1`
- M7: pending - M7 merged on `main`, tagged `operator-ergonomics-v1`
- Release activation: pending - Release 0.0.6 packaged, health-gated activated, full sweep green
- Final suite counts: pending - Suites at end of run: config 24/24, task 32/32, release 14/14, verify PASS
- Corrections encountered: pending - Build log: Phases 9 (M5), 10 (M6), 11 (M7) appended with corrections
- Corrections encountered: dropped constant from a failed atomic edit batch (SUPPORTED_TOOLS); dash `export` output format vs `env`; three selftest authoring defects; showRun id-regex case sensitivity + missing-run crash. All fixed and covered by tests.
- Commits pushed incrementally; nothing left uncommitted
- Live proof: workspace file host-visible; session teach/recall ('mosaico') verified
## Next steps after this run (not started) ## Next steps after this run (not started)
+58 -2
View File
@@ -365,17 +365,69 @@ function listRuns() {
for (const runId of entries) { for (const runId of entries) {
let status = "unknown"; let status = "unknown";
let taskId = "-"; let taskId = "-";
let workspace = "-";
let session = "-";
try { try {
const result = JSON.parse(fs.readFileSync(path.join(root, runId, "result.json"), "utf8")); const result = JSON.parse(fs.readFileSync(path.join(root, runId, "result.json"), "utf8"));
status = result.status; status = result.status;
taskId = result.taskId; taskId = result.taskId;
workspace = result.workspace ?? "-";
session = result.session ?? "-";
} catch { } catch {
// Incomplete run record; report as unknown. // Incomplete run record; report as unknown.
} }
process.stdout.write(`${runId} ${status.padEnd(9)} ${taskId}\n`); process.stdout.write(`${runId} ${status.padEnd(9)} task=${taskId.padEnd(18)} ws=${String(workspace).padEnd(10)} session=${session}\n`);
} }
} }
function showRun(runId) {
if (!/^r-[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(runId)) {
fail(4, `invalid run id: ${JSON.stringify(runId)} (expected r-<id>)`);
}
const resolved = loadConfig();
const dir = path.join(runsRoot(resolved), runId);
if (!fs.existsSync(dir)) {
fail(4, `run not found: ${runId} (under ${runsRoot(resolved)})`);
}
const read = (name) => {
try {
return JSON.parse(fs.readFileSync(path.join(dir, name), "utf8"));
} catch {
return null;
}
};
const result = read("result.json");
const task = read("task.json");
const mission = read("mission.json");
process.stdout.write(`run: ${runId}\n`);
if (result) {
process.stdout.write(
[
`status: ${result.status}${result.reason ? ` (${result.reason})` : ""}`,
`task: ${result.taskId}`,
result.missionId ? `mission: ${result.missionId}` : null,
result.workspace ? `workspace: ${result.workspace}` : null,
result.session ? `session: ${result.session}` : null,
result.tools ? `tools: ${result.tools.join(", ")}` : null,
`adapter: ${"(see config)"} provider=${result.provider} model=${result.model}`,
`request: ${JSON.stringify(result.request)}`,
`response: ${JSON.stringify(result.response)}`,
result.expectedExact !== null && result.expectedExact !== undefined ? `expected: ${JSON.stringify(result.expectedExact)}` : null,
`timing: ${result.startedAt} -> ${result.finishedAt} (${result.durationMs} ms)`,
`exit: ${result.exitCode}${result.signal ? ` signal=${result.signal}` : ""}`,
].filter((line) => line !== null).join("\n") + "\n",
);
} else {
process.stdout.write("result.json: (missing or unreadable)\n");
}
if (task) process.stdout.write(`task snapshot: ${"task.json"} present\n`);
if (mission) process.stdout.write(`mission snapshot: ${mission.id} - ${mission.objective}\n`);
process.stdout.write(`artifacts: ${fs.readdirSync(dir).map((f) => `${f}`).join(", ")}\n`);
process.exit(0);
}
const operation = process.argv[2]; const operation = process.argv[2];
const target = process.argv[3]; const target = process.argv[3];
@@ -392,9 +444,13 @@ switch (operation) {
if (!target) fail(4, "usage: mosaic-task.mjs run <taskFile>"); if (!target) fail(4, "usage: mosaic-task.mjs run <taskFile>");
runTask(path.resolve(target)); runTask(path.resolve(target));
break; break;
case "show":
if (!target) fail(4, "usage: mosaic-task.mjs show <runId>");
showRun(target);
break;
case "list": case "list":
listRuns(); listRuns();
process.exit(0); process.exit(0);
default: default:
fail(4, `unknown operation: ${JSON.stringify(operation ?? "")} (expected validate | run | list)`); fail(4, `unknown operation: ${JSON.stringify(operation ?? "")} (expected validate | run | show | list)`);
} }