Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fad8a4718c | ||
|
|
2ff49adff4 | ||
|
|
44c476ebbf | ||
|
|
cde480eb60 | ||
|
|
5808248707 | ||
|
|
afd5827db8 | ||
|
|
d9cc990376 | ||
|
|
83c4e9851e | ||
|
|
22508170a2 | ||
|
|
90a67d050e | ||
|
|
24bdef75fa | ||
|
|
4e2a413640 | ||
|
|
be55549700 | ||
|
|
ddb1554e5b | ||
|
|
b017e66e17 | ||
|
|
172368612c | ||
|
|
1387231e57 |
@@ -221,4 +221,86 @@ 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`.
|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
|
||||
---
|
||||
|
||||
## Phase 12: Conductor loop — self-orchestration (M8)
|
||||
|
||||
### Entry 12.1 — before
|
||||
|
||||
- Timestamp: 2026-09-03
|
||||
- Intended action: Stand up the poor-man orchestration loop per docs/plans/CONDUCTOR.md: conductor (host, holds git) mirrors the repo into a worker workspace, dispatches a headless pi worker (session worker-1, tools read/write/edit/bash) to implement retry <runId>, reviews the diff, integrates, verifies (Gitea #25, #26, #27).
|
||||
- Reason: The owner asked for circular task processing with agent workers; the stack now has every primitive needed — this proves it on the stack itself.
|
||||
- Expected result: worker-authored retry merged with suites green and a live retry verified.
|
||||
|
||||
### Entry 12.2 — after
|
||||
|
||||
- Timestamp: 2026-09-03
|
||||
- Commands run: repo mirror clone; worker dispatch (tasks/worker-retry.json); diff review; apply; live retry; refinement dispatch (tasks/worker-retry-refine.json); second review; reverse+reapply combined patch; conductor interpolation fix; live retry; full sweep.
|
||||
- Observed result:
|
||||
- Worker round 1: implemented retry correctly per spec in 2m28s; diff reviewed clean.
|
||||
- Live retry exposed a spec gap (direct invocation lacks launcher env exports).
|
||||
- Worker round 2 (same session, 59s): made spawnEnv self-sufficient, but used PI_* names where compose interpolates MOSAIC_*.
|
||||
- Conductor hotfix: 3-line rename to MOSAIC_PROVIDER/MOSAIC_MODEL/MOSAIC_DATA_ROOT (too trivial for a worker round).
|
||||
- Final: live retry succeeded (replied REMEMBERED, new run recorded); all suites green.
|
||||
- Failure or correction: three rounds total — one spec gap (conductor), one naming mismatch (worker), one trivial rename (conductor). Each was caught by mechanical verification (run record stderr), never by hope.
|
||||
- Attribution: feature authored by headless pi worker (glm-5.3-flash) in sessions worker-1; conductor reviewed, integrated, and hotfixed.
|
||||
|
||||
## Result (M8)
|
||||
|
||||
Conductor loop proven end-to-end on the stack itself. `main` merged with M8; release 0.0.6 remains active (retry is host-side only, no image change).
|
||||
|
||||
|
||||
|
||||
@@ -119,6 +119,31 @@ adapters/<name>/adapter.sh env in: MOSAIC_SYSTEM_PROMPT_FILE, MOSAIC_REQUEST,
|
||||
|
||||
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.
|
||||
|
||||
Inside the container:
|
||||
|
||||
@@ -10,4 +10,7 @@ set -eu
|
||||
[ -r "$MOSAIC_SYSTEM_PROMPT_FILE" ] || { echo "mock adapter: system prompt not readable: $MOSAIC_SYSTEM_PROMPT_FILE" >&2; exit 2; }
|
||||
|
||||
echo "mock adapter: responding verbatim from MOSAIC_MOCK_RESPONSE" >&2
|
||||
# Deterministic plumbing evidence: which MOSAIC_* variables did the
|
||||
# orchestrator actually deliver? (Auth secrets are not MOSAIC_-prefixed.)
|
||||
(env | grep '^MOSAIC_' | sort) >&2 2>/dev/null || true
|
||||
printf '%s\n' "${MOSAIC_MOCK_RESPONSE:-}"
|
||||
|
||||
+25
-3
@@ -12,21 +12,43 @@ set -eu
|
||||
: "${PI_PROVIDER:?pi adapter: PI_PROVIDER is required}"
|
||||
: "${PI_MODEL:?pi adapter: PI_MODEL is required}"
|
||||
|
||||
# Workspace (M5): run inside the provided workspace when present.
|
||||
if [ -n "${MOSAIC_WORKSPACE:-}" ]; then
|
||||
mkdir -p "$MOSAIC_WORKSPACE"
|
||||
cd "$MOSAIC_WORKSPACE"
|
||||
fi
|
||||
|
||||
# Session (M6): persistent named session directory; resume the most recent
|
||||
# session in that directory when one exists (pi documented flags).
|
||||
# Default remains ephemeral (--no-session) when no session is declared.
|
||||
SESSION_FLAGS="--no-session"
|
||||
if [ -n "${MOSAIC_SESSION_DIR:-}" ]; then
|
||||
mkdir -p "$MOSAIC_SESSION_DIR"
|
||||
SESSION_FLAGS="--session-dir $MOSAIC_SESSION_DIR"
|
||||
if [ -n "$(ls -A "$MOSAIC_SESSION_DIR" 2>/dev/null)" ]; then
|
||||
SESSION_FLAGS="$SESSION_FLAGS -c"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Capabilities (M5): explicit allowlist or no tools.
|
||||
TOOLS_FLAG="--no-tools"
|
||||
[ -n "${MOSAIC_TOOLS:-}" ] && TOOLS_FLAG="--tools $MOSAIC_TOOLS"
|
||||
|
||||
# All flags documented in the pi package README (CLI Reference):
|
||||
# -p/--print noninteractive: print the response and exit
|
||||
# --system-prompt replace the default prompt with the generated one
|
||||
# --no-* no ambient context/skills/extensions/templates/themes
|
||||
# --no-session ephemeral; --no-tools this runtime needs no tools
|
||||
# --no-session ephemeral; TOOLS_FLAG per capabilities
|
||||
# --offline no startup network operations (update checks/telemetry)
|
||||
exec pi \
|
||||
--offline \
|
||||
--no-session \
|
||||
--no-extensions \
|
||||
--no-skills \
|
||||
--no-prompt-templates \
|
||||
--no-themes \
|
||||
--no-context-files \
|
||||
--no-tools \
|
||||
$TOOLS_FLAG \
|
||||
$SESSION_FLAGS \
|
||||
--provider "$PI_PROVIDER" \
|
||||
--model "$PI_MODEL" \
|
||||
--system-prompt "$(cat "$MOSAIC_SYSTEM_PROMPT_FILE")" \
|
||||
|
||||
@@ -15,6 +15,11 @@ services:
|
||||
# Mission directives injection point (set by the task runner when the
|
||||
# task references a mission; container path of the run snapshot)
|
||||
MOSAIC_MISSION_FILE: ${MOSAIC_MISSION_FILE:-}
|
||||
# Workspace + capabilities (set by the task runner; M5)
|
||||
MOSAIC_WORKSPACE: ${MOSAIC_WORKSPACE:-}
|
||||
MOSAIC_TOOLS: ${MOSAIC_TOOLS:-}
|
||||
# Persistent named session dir (set by the task runner; M6)
|
||||
MOSAIC_SESSION_DIR: ${MOSAIC_SESSION_DIR:-}
|
||||
# mock adapter only: verbatim response for deterministic seam tests
|
||||
MOSAIC_MOCK_RESPONSE: ${MOSAIC_MOCK_RESPONSE:-}
|
||||
# Documented container auth alternative: provider API key via
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
# Autonomous Work Run — 2026-09-03
|
||||
|
||||
**Status:** COMPLETED (single-session batch; see Results at bottom)
|
||||
**Constraint:** The assistant cannot run unattended. This was one long interactive session, not 12 wall-clock hours. Everything below was completed, committed, and pushed during that session.
|
||||
|
||||
## Objective
|
||||
|
||||
Advance the Mosaic Stack rebuild several verified layers in one batch, focused on Pi, ending in a state the owner can test and review alone: green suites, activated release, recorded drills, and this document as the single entry point.
|
||||
|
||||
## Scope decided for this run
|
||||
|
||||
| Milestone | Theme | Status |
|
||||
|---|---|---|
|
||||
| M5 | Task workspaces + capability envelope (tools allowlist) | DONE |
|
||||
| M6 | Named sessions — persistence and resume (L1) | DONE |
|
||||
| M7 | Operator ergonomics: run inspection commands | DONE |
|
||||
| — | Releases 0.0.5+0.0.6 packaged; 0.0.6 health-gated activated | DONE |
|
||||
|
||||
Explicitly deferred (do not mistake for forgotten):
|
||||
- Claude/Codex/OpenCode adapters (owner: focus on Pi for now)
|
||||
- Network policy engine (container boundary is the current control)
|
||||
- Fine-grained read restrictions (excluded by the original brief)
|
||||
- Config/state migrations (no schema breaks so far; keep it that way)
|
||||
|
||||
## Design decisions taken during this run
|
||||
|
||||
1. **Workspace** (`task.workspace`, optional):
|
||||
- absent → tool-free text-only run (previous behavior, unchanged)
|
||||
- `":run"` → ephemeral per-run workspace at `<dataRoot>/runs/<runId>/workspace`
|
||||
- named (validated id) → persistent shared workspace at `<dataRoot>/workspaces/<name>`
|
||||
- Container path passed via `MOSAIC_WORKSPACE` env; adapter cds into it. No new mounts (dataRoot is already mounted).
|
||||
2. **Capabilities** (`task.capabilities.tools`, optional): allowlist from pi's documented tool set (`read write edit bash grep find ls`). Absent → `--no-tools` (previous behavior). Passed via `MOSAIC_TOOLS` env; pi adapter maps to `--tools`.
|
||||
3. **Adapter diagnostics for deterministic testing**: the mock adapter writes all received `MOSAIC_*` variables (never secrets — auth is not MOSAIC_-prefixed) to stderr, which lands in the run record. This lets selftests assert orchestrator→adapter plumbing without parsing model output.
|
||||
4. **Sessions** (`task.session`, optional named): persisted under `<dataRoot>/sessions/<name>/` via pi's documented `--session-dir`; resume semantics: continue most recent session in that directory when one exists (`-c`).
|
||||
5. **Selection authority unchanged**: config file for adapter/provider/model; task file for workspace/capabilities/session; env vars are internal plumbing only.
|
||||
6. **configVersion stays 1**; all new task fields are optional. Old tasks/configs remain valid.
|
||||
|
||||
## Test plan (what "done" means per milestone)
|
||||
|
||||
- M5: mock-adapter cases asserting workspace path and tools arrive via run-record stderr; live pi case writing/reading a file in a persistent workspace; validation negatives (bad tool name, bad workspace name)
|
||||
- M6: session directory deterministically populated after first run; second run resumes (continuation asserted by session dir state and, in live E2E, by model recall); sandbox isolation between two named sessions
|
||||
- M7: `show <runId>` prints a complete run record; `list` gains workspace/session columns
|
||||
- Final: full sweep (config/task/release), verify, package + activate 0.0.6, config checksum unchanged
|
||||
|
||||
## Review checklist for the owner
|
||||
|
||||
1. `cat docs/plans/2026-09-03_autonomous-run.md` (this file)
|
||||
2. `scripts/release.sh status` → 0.0.6 active
|
||||
3. `scripts/test-config.sh && scripts/test-task.sh && scripts/test-release.sh && scripts/verify.sh`
|
||||
4. Try a workspace task:
|
||||
```bash
|
||||
scripts/run-task.sh run tasks/workspace-demo.json
|
||||
ls ~/.mosaic-dev/workspaces/demo/
|
||||
```
|
||||
5. Try the session demo:
|
||||
```bash
|
||||
scripts/run-task.sh run tasks/session-demo-1.json # teaches a word
|
||||
scripts/run-task.sh run tasks/session-demo-2.json # recalls it
|
||||
```
|
||||
6. Inspect any run: `node scripts/mosaic-task.mjs show <runId>`
|
||||
7. Gitea: milestones M5/M6/M7 closed; issues referenced by merge commits
|
||||
|
||||
## Results
|
||||
|
||||
- M5 merged on `main` (merge commit `ddb1554`), tagged `workspace-capabilities-v1`
|
||||
- M6 merged on `main` (merge commit `4e2a413`), tagged `sessions-v1`
|
||||
- M7 merged on `main`, tagged `operator-ergonomics-v1`
|
||||
- Release 0.0.6 packaged, health-gated activated, full sweep green
|
||||
- Suites at end of run: config 24/24, task 32/32, release 14/14, verify PASS
|
||||
- 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)
|
||||
|
||||
1. Owner review + hands-on testing of workspaces, capabilities, sessions
|
||||
2. Decision: capability defaults per mission (mission-level policy) — natural M8
|
||||
3. Second real adapter remains available whenever wanted
|
||||
4. Consider run-record pruning/retention policy once run volume grows
|
||||
5. Consider a `mosaic-task.mjs retry <runId>` convenience for failed runs
|
||||
@@ -0,0 +1,51 @@
|
||||
# Conductor protocol — poor-man orchestration loop
|
||||
|
||||
How the stack orchestrates headless pi workers to do work on itself.
|
||||
|
||||
## Roles
|
||||
|
||||
| Role | Runs where | Powers | Never has |
|
||||
|---|---|---|---|
|
||||
| **Conductor** | host (assistant or owner) | git (clone/commit/push), task dispatch, review, verification suites, Gitea | nothing new |
|
||||
| **Worker** | container (headless pi via `scripts/run-task.sh`) | read/write/edit/bash inside its workspace; persistent session on request | git credentials, docker socket, host filesystem |
|
||||
|
||||
## The loop
|
||||
|
||||
1. **Decompose**: conductor turns a goal into worker tasks small enough to
|
||||
specify completely in one prompt (file paths, acceptance criteria, style
|
||||
constraints, verification the worker can run itself, e.g. `node --check`).
|
||||
2. **Mirror**: conductor maintains the repo clone at
|
||||
`<dataRoot>/workspaces/stack-repo` (host-side git; workers see it read-write
|
||||
through their workspace mount).
|
||||
3. **Dispatch**: `scripts/run-task.sh run <worker-task.json>` — worker edits the
|
||||
clone. Session name `worker-<n>` keeps continuity across refinement rounds.
|
||||
4. **Extract**: `git -C <workspace> diff > patch` — the worker's entire output
|
||||
is a reviewable diff. Run record (result.json, stderr.txt) is the receipt.
|
||||
5. **Review**: conductor reads the diff line by line. Bad output → refine the
|
||||
prompt, re-dispatch (same session: "your patch had these problems…").
|
||||
6. **Integrate**: conductor applies the patch to the real repo, runs the full
|
||||
suites, commits and pushes. Suites failing → revert apply, back to step 5.
|
||||
7. **Record**: update CURRENT.md, BUILD-LOG, close the Gitea issue.
|
||||
|
||||
## Guardrails
|
||||
|
||||
- Workers never receive credentials; they never run git; they never leave the
|
||||
workspace (container is the boundary; tools allowlist is the gate).
|
||||
- Every worker diff is reviewed by the conductor before integration. No
|
||||
auto-apply. (Auto-apply would be a capability-policy decision for later.)
|
||||
- Verification is mechanical: suites + `node --check` / `bash -n` gates.
|
||||
- Recursive decomposition = "fail → smaller task", never "hope."
|
||||
|
||||
## Worker task template
|
||||
|
||||
```json
|
||||
{
|
||||
"taskVersion": 1,
|
||||
"id": "t-worker-<name>",
|
||||
"prompt": "<full spec: goal, files, constraints, acceptance, self-checks>",
|
||||
"workspace": "stack-repo",
|
||||
"capabilities": { "tools": ["read", "write", "edit", "bash"] },
|
||||
"session": "worker-1",
|
||||
"timeoutSeconds": 600
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,36 @@
|
||||
# CURRENT — single source of "what happens next"
|
||||
|
||||
This file always names exactly one next action. Any "continue" / "next" /
|
||||
"proceed" message means: execute the action below, fully (implement → test →
|
||||
verify against its acceptance criteria → commit → push → close the issue →
|
||||
update this file to the next action). No ambiguity, no re-planning.
|
||||
|
||||
## Next action
|
||||
|
||||
Owner decision on M9: mission-level capability policy (design sketch: missions may declare default tool sets; tasks inherit unless overridden; conductor validates the merge). Say "next" to proceed or name a different target.
|
||||
|
||||
## Queue (ordered, not started)
|
||||
|
||||
1. M9: mission-level capability policy
|
||||
2. Run-record retention/pruning policy
|
||||
3. Second real adapter (parked — owner focused on Pi)
|
||||
4. Auto-apply policy for worker patches (deferred until capability policy exists)
|
||||
|
||||
## Rules
|
||||
|
||||
- One action in flight. Update this file at the END of every action.
|
||||
- Blocked? Move the item to "Blocked" below with the reason and stop.
|
||||
- Completed actions move to the log at the bottom (date + issue + result).
|
||||
|
||||
## Blocked
|
||||
|
||||
(none)
|
||||
|
||||
## Completed log
|
||||
|
||||
- 2026-09-03 — M5 workspaces + capability envelope (#20, #21) — merged, tests green
|
||||
- 2026-09-03 — M6 named sessions + resume (#22, #23) — merged, teach/recall verified
|
||||
- 2026-09-03 — M7 run inspection + release 0.0.6 (#24) — merged, activated
|
||||
- 2026-09-03 — M8 conductor loop + worker-built retry (#25, #26, #27) — merged; worker authored retry in 2 refinement rounds, conductor fixed a 3-line interpolation rename; live retry verified
|
||||
- 2026-09-03 — retry lineage + relative mission resolution (#28) — merged, 36/24/14 suites + verify green
|
||||
- 2026-09-03 — M8 conductor loop + worker-built retry (#25, #26, #27) — merged; worker authored retry in 2 refinement rounds, conductor fixed a 3-line interpolation rename; live retry verified
|
||||
+236
-6
@@ -28,6 +28,7 @@
|
||||
*/
|
||||
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import process from "node:process";
|
||||
import { randomBytes } from "node:crypto";
|
||||
@@ -38,6 +39,7 @@ const PROJECT_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)),
|
||||
const RUNS_DIRNAME = "runs";
|
||||
const ID_PATTERN = /^[a-z0-9][a-z0-9._-]{0,63}$/;
|
||||
const DEFAULT_TIMEOUT_SECONDS = 120;
|
||||
const SUPPORTED_TOOLS = ["read", "write", "edit", "bash", "grep", "find", "ls"]; // pi documented built-ins
|
||||
|
||||
function fail(exitCode, message) {
|
||||
process.stderr.write(`mosaic-task: ${message}\n`);
|
||||
@@ -82,7 +84,7 @@ function validateId(value, what) {
|
||||
|
||||
function validateMission(document, file) {
|
||||
if (!isPlainObject(document)) fail(2, "mission must be a JSON object");
|
||||
rejectUnknownKeys(document, ["missionVersion", "id", "objective", "directives"], "mission");
|
||||
rejectUnknownKeys(document, ["missionVersion", "id", "objective", "directives", "capabilities"], "mission");
|
||||
if (document.missionVersion !== 1) {
|
||||
fail(2, `unsupported missionVersion: ${JSON.stringify(document.missionVersion)} (supported: 1)`);
|
||||
}
|
||||
@@ -100,12 +102,34 @@ function validateMission(document, file) {
|
||||
return d;
|
||||
});
|
||||
}
|
||||
return { missionVersion: document.missionVersion, id: document.id, objective: document.objective, directives };
|
||||
|
||||
// Governing capability constraints (M9): same validation as task
|
||||
// capabilities; semantically these BOUND tasks (least-privilege
|
||||
// intersection at run time), never grant beyond them.
|
||||
let capabilities = null;
|
||||
if (document.capabilities !== undefined && document.capabilities !== null) {
|
||||
if (!isPlainObject(document.capabilities)) fail(2, 'mission "capabilities" must be a JSON object');
|
||||
rejectUnknownKeys(document.capabilities, ["tools"], 'mission "capabilities"');
|
||||
if (!Array.isArray(document.capabilities.tools) || document.capabilities.tools.length === 0) {
|
||||
fail(2, 'mission "capabilities.tools" must be a non-empty array of tool names');
|
||||
}
|
||||
const seen = new Set();
|
||||
for (const tool of document.capabilities.tools) {
|
||||
if (!SUPPORTED_TOOLS.includes(tool)) {
|
||||
fail(2, `unsupported mission tool: ${JSON.stringify(tool)} (supported: ${SUPPORTED_TOOLS.join(", ")})`);
|
||||
}
|
||||
if (seen.has(tool)) fail(2, `duplicate tool in mission capabilities.tools: ${tool}`);
|
||||
seen.add(tool);
|
||||
}
|
||||
capabilities = { tools: [...seen] };
|
||||
}
|
||||
|
||||
return { missionVersion: document.missionVersion, id: document.id, objective: document.objective, directives, capabilities };
|
||||
}
|
||||
|
||||
function validateTask(document, file) {
|
||||
if (!isPlainObject(document)) fail(2, "task must be a JSON object");
|
||||
rejectUnknownKeys(document, ["taskVersion", "id", "prompt", "mission", "expectExact", "timeoutSeconds"], "task");
|
||||
rejectUnknownKeys(document, ["taskVersion", "id", "prompt", "mission", "expectExact", "timeoutSeconds", "workspace", "capabilities", "session"], "task");
|
||||
if (document.taskVersion !== 1) {
|
||||
fail(2, `unsupported taskVersion: ${JSON.stringify(document.taskVersion)} (supported: 1)`);
|
||||
}
|
||||
@@ -144,6 +168,50 @@ function validateTask(document, file) {
|
||||
timeoutSeconds = document.timeoutSeconds;
|
||||
}
|
||||
|
||||
// Workspace (M5): absent = none; ":run" = ephemeral per-run; otherwise a
|
||||
// persistent named workspace under <dataRoot>/workspaces/<name>.
|
||||
let workspace = null;
|
||||
if (document.workspace !== undefined && document.workspace !== null) {
|
||||
if (typeof document.workspace !== "string" || document.workspace.length === 0) {
|
||||
fail(2, 'task "workspace" must be a non-empty string when present');
|
||||
}
|
||||
if (document.workspace !== ":run") {
|
||||
validateId(document.workspace, "task workspace");
|
||||
}
|
||||
workspace = document.workspace;
|
||||
}
|
||||
|
||||
// Session (M6): optional named persistent session under
|
||||
// <dataRoot>/sessions/<name>. Distinct names never share state.
|
||||
let session = null;
|
||||
if (document.session !== undefined && document.session !== null) {
|
||||
if (typeof document.session !== "string" || document.session.length === 0) {
|
||||
fail(2, 'task "session" must be a non-empty string when present');
|
||||
}
|
||||
validateId(document.session, "task session");
|
||||
session = document.session;
|
||||
}
|
||||
|
||||
// Capabilities (M5): optional tools allowlist mapped by adapters to their
|
||||
// native permission flags. Absent = no tools.
|
||||
let tools = null;
|
||||
if (document.capabilities !== undefined && document.capabilities !== null) {
|
||||
if (!isPlainObject(document.capabilities)) fail(2, '"capabilities" must be a JSON object');
|
||||
rejectUnknownKeys(document.capabilities, ["tools"], '"capabilities"');
|
||||
if (!Array.isArray(document.capabilities.tools) || document.capabilities.tools.length === 0) {
|
||||
fail(2, '"capabilities.tools" must be a non-empty array of tool names');
|
||||
}
|
||||
const seen = new Set();
|
||||
for (const tool of document.capabilities.tools) {
|
||||
if (!SUPPORTED_TOOLS.includes(tool)) {
|
||||
fail(2, `unsupported tool: ${JSON.stringify(tool)} (supported: ${SUPPORTED_TOOLS.join(", ")})`);
|
||||
}
|
||||
if (seen.has(tool)) fail(2, `duplicate tool in capabilities.tools: ${tool}`);
|
||||
seen.add(tool);
|
||||
}
|
||||
tools = [...seen];
|
||||
}
|
||||
|
||||
return {
|
||||
taskVersion: document.taskVersion,
|
||||
id: document.id,
|
||||
@@ -153,6 +221,9 @@ function validateTask(document, file) {
|
||||
missionSnapshot,
|
||||
expectExact,
|
||||
timeoutSeconds,
|
||||
workspace,
|
||||
tools,
|
||||
session,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -185,7 +256,7 @@ function writeOnce(file, content) {
|
||||
}
|
||||
}
|
||||
|
||||
function runTask(taskFile) {
|
||||
function runTask(taskFile, options = {}) {
|
||||
const resolved = JSON.parse(
|
||||
spawnSync(process.execPath, [path.join(PROJECT_ROOT, "scripts", "mosaic-config.mjs"), "validate"], {
|
||||
cwd: PROJECT_ROOT,
|
||||
@@ -215,6 +286,15 @@ function runTask(taskFile) {
|
||||
// CONTAINER path (dataRoot maps to /var/lib/mosaic in the image).
|
||||
const spawnEnv = { ...process.env };
|
||||
spawnEnv.MOSAIC_ADAPTER = resolved.execution.adapter;
|
||||
// Self-sufficient env: direct invocation (e.g. `retry`) skips the shell
|
||||
// launcher exports, so derive them from the resolved config and release.
|
||||
// (Names here are the compose interpolation consumers, not PI_*.)
|
||||
spawnEnv.MOSAIC_PROVIDER = resolved.execution.provider;
|
||||
spawnEnv.MOSAIC_MODEL = resolved.execution.model;
|
||||
spawnEnv.MOSAIC_DATA_ROOT = resolved.dataRoot;
|
||||
const release = fs.readFileSync(path.join(PROJECT_ROOT, "RELEASE"), "utf8").trim();
|
||||
const piVersion = JSON.parse(fs.readFileSync(path.join(PROJECT_ROOT, "package.json"), "utf8")).dependencies["@earendil-works/pi-coding-agent"];
|
||||
spawnEnv.MOSAIC_IMAGE_TAG = `mosaic-poc-agent:${piVersion}-r${release}`;
|
||||
if (task.missionSnapshot) {
|
||||
const relative = path.relative(resolved.dataRoot, runDir);
|
||||
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
||||
@@ -223,6 +303,41 @@ function runTask(taskFile) {
|
||||
spawnEnv.MOSAIC_MISSION_FILE = `/var/lib/mosaic/${relative.split(path.sep).join("/")}/mission.json`;
|
||||
}
|
||||
|
||||
// Workspace (M5): create host-side, pass the CONTAINER path.
|
||||
let workspaceContainerPath = null;
|
||||
if (task.workspace === ":run") {
|
||||
fs.mkdirSync(path.join(runDir, "workspace"), { recursive: true });
|
||||
workspaceContainerPath = `/var/lib/mosaic/runs/${runId}/workspace`;
|
||||
} else if (task.workspace) {
|
||||
fs.mkdirSync(path.join(resolved.dataRoot, "workspaces", task.workspace), { recursive: true });
|
||||
workspaceContainerPath = `/var/lib/mosaic/workspaces/${task.workspace}`;
|
||||
}
|
||||
if (workspaceContainerPath) spawnEnv.MOSAIC_WORKSPACE = workspaceContainerPath;
|
||||
|
||||
// Capability policy (M9): least-privilege intersection. A task may narrow
|
||||
// a mission's tool grant, never widen it. Empty intersection = tool-free.
|
||||
let effectiveTools = task.tools;
|
||||
let policyNote = null;
|
||||
if (task.missionSnapshot?.capabilities) {
|
||||
const missionTools = task.missionSnapshot.capabilities.tools;
|
||||
if (effectiveTools) {
|
||||
effectiveTools = effectiveTools.filter((t) => missionTools.includes(t));
|
||||
if (effectiveTools.length === 0) {
|
||||
policyNote = `capability policy: mission ${task.missionSnapshot.id} and task request no tools in common -> tool-free run`;
|
||||
}
|
||||
} else {
|
||||
effectiveTools = [...missionTools];
|
||||
}
|
||||
}
|
||||
if (policyNote) process.stderr.write(`mosaic-task: ${policyNote}\n`);
|
||||
spawnEnv.MOSAIC_TOOLS = effectiveTools ? effectiveTools.join(",") : "";
|
||||
|
||||
// Session (M6): persistent named session dir, passed as container path.
|
||||
if (task.session) {
|
||||
fs.mkdirSync(path.join(resolved.dataRoot, "sessions", task.session), { recursive: true });
|
||||
spawnEnv.MOSAIC_SESSION_DIR = `/var/lib/mosaic/sessions/${task.session}`;
|
||||
}
|
||||
|
||||
const proc = spawnSync(
|
||||
"docker",
|
||||
["compose", "run", "--rm", "-T", "mosaic-agent", task.prompt],
|
||||
@@ -269,6 +384,10 @@ function runTask(taskFile) {
|
||||
request: task.prompt,
|
||||
response,
|
||||
expectedExact: expected,
|
||||
...(options.retriedFrom ? { retriedFrom: options.retriedFrom } : {}),
|
||||
workspace: task.workspace,
|
||||
tools: effectiveTools,
|
||||
session: task.session,
|
||||
exitCode: proc.status,
|
||||
signal: proc.signal ?? null,
|
||||
provider: resolved.execution.provider,
|
||||
@@ -296,17 +415,120 @@ function listRuns() {
|
||||
for (const runId of entries) {
|
||||
let status = "unknown";
|
||||
let taskId = "-";
|
||||
let workspace = "-";
|
||||
let session = "-";
|
||||
try {
|
||||
const result = JSON.parse(fs.readFileSync(path.join(root, runId, "result.json"), "utf8"));
|
||||
status = result.status;
|
||||
taskId = result.taskId;
|
||||
workspace = result.workspace ?? "-";
|
||||
session = result.session ?? "-";
|
||||
} catch {
|
||||
// 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,
|
||||
result.retriedFrom ? `retriedFrom: ${result.retriedFrom}` : 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);
|
||||
}
|
||||
|
||||
function retryRun(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}`);
|
||||
}
|
||||
|
||||
let snapshot;
|
||||
try {
|
||||
snapshot = fs.readFileSync(path.join(dir, "task.json"), "utf8");
|
||||
} catch {
|
||||
fail(4, `run task snapshot unreadable: ${runId}`);
|
||||
}
|
||||
|
||||
// Relative mission paths in a snapshot resolve against the ORIGINAL task
|
||||
// location, which no longer exists here — rewrite them to the run's own
|
||||
// recorded mission.json so retries stay faithful.
|
||||
let snapshotDoc;
|
||||
try {
|
||||
snapshotDoc = JSON.parse(snapshot);
|
||||
} catch {
|
||||
fail(4, `run task snapshot is not valid JSON: ${runId}`);
|
||||
}
|
||||
if (snapshotDoc.mission && !path.isAbsolute(snapshotDoc.mission)) {
|
||||
const recordedMission = path.join(dir, "mission.json");
|
||||
if (!fs.existsSync(recordedMission)) {
|
||||
fail(4, `cannot retry ${runId}: relative mission path but no mission.json snapshot in run dir`);
|
||||
}
|
||||
snapshotDoc.mission = recordedMission;
|
||||
snapshot = `${JSON.stringify(snapshotDoc, null, 2)}\n`;
|
||||
}
|
||||
|
||||
// A retry is a brand-new run: replay the recorded task snapshot through
|
||||
// the ordinary run path; existing run records stay untouched.
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "mosaic-retry-"));
|
||||
const tempTaskFile = path.join(tempDir, "task.json");
|
||||
fs.writeFileSync(tempTaskFile, snapshot);
|
||||
process.on("exit", () => {
|
||||
try {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
} catch {
|
||||
// Best-effort cleanup only.
|
||||
}
|
||||
});
|
||||
runTask(tempTaskFile, { retriedFrom: runId });
|
||||
}
|
||||
|
||||
const operation = process.argv[2];
|
||||
const target = process.argv[3];
|
||||
|
||||
@@ -323,9 +545,17 @@ switch (operation) {
|
||||
if (!target) fail(4, "usage: mosaic-task.mjs run <taskFile>");
|
||||
runTask(path.resolve(target));
|
||||
break;
|
||||
case "show":
|
||||
if (!target) fail(4, "usage: mosaic-task.mjs show <runId>");
|
||||
showRun(target);
|
||||
break;
|
||||
case "list":
|
||||
listRuns();
|
||||
process.exit(0);
|
||||
case "retry":
|
||||
if (!target) fail(4, "usage: mosaic-task.mjs retry <runId>");
|
||||
retryRun(target);
|
||||
break;
|
||||
default:
|
||||
fail(4, `unknown operation: ${JSON.stringify(operation ?? "")} (expected validate | run | list)`);
|
||||
fail(4, `unknown operation: ${JSON.stringify(operation ?? "")} (expected validate | run | show | list | retry)`);
|
||||
}
|
||||
|
||||
@@ -142,10 +142,86 @@ EOF
|
||||
&& grep -q 'Seam directive A.' "$SANDBOX/data/system-prompt.md" \
|
||||
&& check "mission section injected into generated prompt" 0 \
|
||||
|| check "mission section injected into generated prompt" 1
|
||||
|
||||
# retry lineage + relative mission path resolution
|
||||
expect_exit "retry of mission run succeeds" 0 -- \
|
||||
env MOSAIC_CONFIG="$SANDBOX/mock-adapters.json" MOSAIC_MOCK_RESPONSE=MOCKED \
|
||||
node scripts/mosaic-task.mjs retry "$(ls -dt "$SANDBOX/data/runs"/r-* | head -1 | xargs basename)"
|
||||
RT="$(ls -dt "$SANDBOX/data/runs"/r-* | head -1 | xargs basename)"
|
||||
ORIG="$(ls -dt "$SANDBOX/data/runs"/r-* | sed -n 2p | xargs basename)"
|
||||
node -e 'const r=JSON.parse(require("fs").readFileSync(process.argv[1],"utf8"));process.exit(r.retriedFrom===process.argv[2]?0:1)' \
|
||||
"$SANDBOX/data/runs/$RT/result.json" "$ORIG" \
|
||||
&& check "retriedFrom lineage recorded" 0 || check "retriedFrom lineage recorded" 1
|
||||
grep -q 'MISSION (runtime)' "$SANDBOX/data/system-prompt.md" \
|
||||
&& check "mission section present after retry (relative path resolved)" 0 \
|
||||
|| check "mission section present after retry (relative path resolved)" 1
|
||||
expect_exit "retry of missing run exits 4" 4 -- \
|
||||
env MOSAIC_CONFIG="$SANDBOX/mock-adapters.json" node scripts/mosaic-task.mjs retry r-missing
|
||||
|
||||
# capability policy (M9): least-privilege intersection
|
||||
POL="$SANDBOX/data/workspaces"; mkdir -p "$POL"
|
||||
pol_run() { # missionTools(ABSENT|json) taskTools(ABSENT|json) -> stderr MOSAIC_TOOLS value
|
||||
if [ "$1" = "ABSENT" ]; then
|
||||
printf '{"missionVersion":1,"id":"m-pol","objective":"o"}' > "$SANDBOX/pol-m.json"
|
||||
else
|
||||
printf '{"missionVersion":1,"id":"m-pol","objective":"o","capabilities":{"tools":[%s]}}' "$1" > "$SANDBOX/pol-m.json"
|
||||
fi
|
||||
if [ "$2" = "ABSENT" ]; then
|
||||
printf '{"taskVersion":1,"id":"t-pol","prompt":"x","mission":"pol-m.json"}' > "$SANDBOX/pol-t.json"
|
||||
else
|
||||
printf '{"taskVersion":1,"id":"t-pol","prompt":"x","mission":"pol-m.json","capabilities":{"tools":[%s]}}' "$2" > "$SANDBOX/pol-t.json"
|
||||
fi
|
||||
env MOSAIC_CONFIG="$SANDBOX/mock-adapters.json" MOSAIC_MOCK_RESPONSE=MOCKED \
|
||||
scripts/run-task.sh run "$SANDBOX/pol-t.json" >/dev/null 2>&1
|
||||
grep -o '^MOSAIC_TOOLS=.*' "$(ls -dt "$SANDBOX/data/runs"/r-* | head -1)/stderr.txt" 2>/dev/null
|
||||
}
|
||||
[ "$(pol_run '"read","bash"' ABSENT)" = 'MOSAIC_TOOLS=read,bash' ] \
|
||||
&& check "policy: mission only -> mission tools" 0 || check "policy: mission only -> mission tools" 1
|
||||
[ "$(pol_run ABSENT '"read","bash"')" = 'MOSAIC_TOOLS=read,bash' ] \
|
||||
&& check "policy: task only -> task tools" 0 || check "policy: task only -> task tools" 1
|
||||
[ "$(pol_run '"read"' '"bash","read"')" = 'MOSAIC_TOOLS=read' ] \
|
||||
&& check "policy: both -> intersection (task narrowed)" 0 || check "policy: both -> intersection (task narrowed)" 1
|
||||
[ "$(pol_run '"grep"' '"bash","read"')" = 'MOSAIC_TOOLS=' ] \
|
||||
&& check "policy: empty intersection -> tool-free" 0 || check "policy: empty intersection -> tool-free" 1
|
||||
printf '{"missionVersion":1,"id":"m-pol","objective":"o","capabilities":{"tools":["sudo"]}}' > "$SANDBOX/pol-m.json"
|
||||
expect_exit "invalid mission capabilities rejected" 2 -- \
|
||||
env MOSAIC_CONFIG="$SANDBOX/mock-adapters.json" $TASK validate "$SANDBOX/pol-t.json"
|
||||
else
|
||||
echo "skip adapter seam cases (docker daemon unavailable)"
|
||||
fi
|
||||
|
||||
# ---------- workspace + capabilities (M5): deterministic mock cases ----------
|
||||
if docker info >/dev/null 2>&1; then
|
||||
printf '{"taskVersion":1,"id":"t-ws","prompt":"ignored","workspace":"suitews","capabilities":{"tools":["read","bash"]}}' > "$SANDBOX/ws-task.json"
|
||||
expect_exit "workspace+tools task runs via mock" 0 -- \
|
||||
env MOSAIC_CONFIG="$SANDBOX/mock-adapters.json" MOSAIC_MOCK_RESPONSE=MOCKED \
|
||||
scripts/run-task.sh run "$SANDBOX/ws-task.json"
|
||||
WSL="$(ls -dt "$SANDBOX/data/runs"/r-* 2>/dev/null | head -1)"
|
||||
grep -q '^MOSAIC_WORKSPACE=/var/lib/mosaic/workspaces/suitews$' "$WSL/stderr.txt" 2>/dev/null \
|
||||
&& grep -q '^MOSAIC_TOOLS=read,bash$' "$WSL/stderr.txt" 2>/dev/null \
|
||||
&& check "workspace path + tools delivered to adapter" 0 \
|
||||
|| check "workspace path + tools delivered to adapter" 1
|
||||
[ -d "$SANDBOX/data/workspaces/suitews" ] \
|
||||
&& check "persistent workspace created on host" 0 \
|
||||
|| check "persistent workspace created on host" 1
|
||||
|
||||
expect_exit "plain task still runs (no workspace/tools)" 0 -- \
|
||||
env MOSAIC_CONFIG="$SANDBOX/mock-adapters.json" MOSAIC_MOCK_RESPONSE=MOSAIC_HELLO_OK \
|
||||
scripts/run-task.sh run "$SANDBOX/ok.json"
|
||||
PL="$(ls -dt "$SANDBOX/data/runs"/r-* 2>/dev/null | head -1)"
|
||||
grep -q '^MOSAIC_WORKSPACE=$' "$PL/stderr.txt" 2>/dev/null \
|
||||
&& check "workspace var present but empty when absent" 0 || check "workspace var present but empty when absent" 1
|
||||
grep -q '^MOSAIC_TOOLS=$' "$PL/stderr.txt" 2>/dev/null \
|
||||
&& check "tools empty when absent" 0 || check "tools empty when absent" 1
|
||||
|
||||
printf '{"taskVersion":1,"id":"t-badtool","prompt":"x","capabilities":{"tools":["sudo"]}}' > "$SANDBOX/badtool.json"
|
||||
expect_exit "unknown tool exits 2" 2 -- $TASK validate "$SANDBOX/badtool.json"
|
||||
printf '{"taskVersion":1,"id":"t-badws","prompt":"x","workspace":"../escape"}' > "$SANDBOX/badws.json"
|
||||
expect_exit "workspace traversal exits 2" 2 -- $TASK validate "$SANDBOX/badws.json"
|
||||
else
|
||||
echo "skip workspace/capability cases (docker daemon unavailable)"
|
||||
fi
|
||||
|
||||
# ---------- live: real runs (Docker + credentials required) ----------
|
||||
# On failure, surface the run record + agent stderr BEFORE the sandbox
|
||||
# cleanup destroys them. Never let a wrong-exit mask the real reason.
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"taskVersion": 1,
|
||||
"id": "t-session-teach",
|
||||
"prompt": "Remember this code word for later: mosaico. Reply with exactly: REMEMBERED",
|
||||
"session": "demo",
|
||||
"expectExact": "REMEMBERED",
|
||||
"timeoutSeconds": 180
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"taskVersion": 1,
|
||||
"id": "t-session-recall",
|
||||
"prompt": "What code word did I ask you to remember earlier in this session? Reply with only the code word.",
|
||||
"session": "demo",
|
||||
"timeoutSeconds": 180
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"taskVersion": 1,
|
||||
"id": "t-worker-retry-refine",
|
||||
"prompt": "Follow-up on your last change to scripts/mosaic-task.mjs (the retry operation). Conductor review found one defect: runTask builds spawnEnv from process.env, so a direct 'node scripts/mosaic-task.mjs retry <runId>' invocation lacks the launcher-exported variables and compose fails with: required variable MOSAIC_IMAGE_TAG is missing.\n\nFIX (in runTask, not retryRun): make spawnEnv self-sufficient —\n1. spawnEnv.PI_PROVIDER = resolved.execution.provider; spawnEnv.PI_MODEL = resolved.execution.model (from the already-loaded resolved config).\n2. spawnEnv.MOSAIC_IMAGE_TAG = 'mosaic-poc-agent:' + <pinned pi version> + '-r' + <RELEASE file content trimmed>, reading RELEASE and package.json the same way scripts/common.sh load_release does.\nKeep the change minimal; do not touch other files; keep code style.\n\nSELF-CHECK: node --check scripts/mosaic-task.mjs must pass.\nWhen finished, reply with exactly: REFINE_DONE",
|
||||
"workspace": "stack-repo",
|
||||
"capabilities": { "tools": ["read", "write", "edit", "bash"] },
|
||||
"session": "worker-1",
|
||||
"timeoutSeconds": 600
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"taskVersion": 1,
|
||||
"id": "t-worker-retry",
|
||||
"prompt": "You are working alone inside a git repository checkout (your current working directory). Implement a new feature in scripts/mosaic-task.mjs.\n\nFEATURE: add a 'retry <runId>' operation alongside the existing 'validate | run | show | list' operations.\n\nBEHAVIOR: 'retry <runId>' re-executes a previously recorded run. Steps: (1) validate the runId argument with the same pattern showRun uses; (2) load the config exactly like showRun does; (3) locate the run directory under the runs root; if it does not exist, fail with exit code 4 and the message 'run not found: <runId>'; (4) read that run directory's task.json snapshot; if unreadable, fail with exit code 4; (5) write the snapshot to a temp file and execute it through the EXISTING runTask(path) function so a brand-new run is created — do not duplicate any execution logic; (6) exit with whatever exit code the new run produced.\n\nCONSTRAINTS: reuse runTask; do not refactor unrelated code; do not touch any file other than scripts/mosaic-task.mjs; keep the existing code style; update the default usage error message to include retry.\n\nSELF-CHECK (run it yourself with the bash tool): node --check scripts/mosaic-task.mjs must pass.\n\nWhen finished, reply with exactly: RETRY_DONE",
|
||||
"workspace": "stack-repo",
|
||||
"capabilities": { "tools": ["read", "write", "edit", "bash"] },
|
||||
"session": "worker-1",
|
||||
"timeoutSeconds": 600
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"taskVersion": 1,
|
||||
"id": "t-workspace-demo",
|
||||
"prompt": "Use the bash tool to create a file named proof.txt in the current directory containing exactly the text: workspace works. Then reply with exactly: WORKSPACE_OK",
|
||||
"workspace": "demo",
|
||||
"capabilities": { "tools": ["bash", "read", "write"] },
|
||||
"expectExact": "WORKSPACE_OK",
|
||||
"timeoutSeconds": 180
|
||||
}
|
||||
Reference in New Issue
Block a user