feat(agents): darkwing host-dev launcher + entry-point consolidation
scripts/agent.sh --host-dev delegates to scripts/agent-host-dev.sh; agents/darkwing/launch.sh provides the native development TUI (context, skills, coding tools, /goal). Root SOUL.md is the M14-era default-collaborator persona captured by the ACT-1 context work. Launcher regression checks pass (test-darkwing-launch.mjs).
This commit is contained in:
@@ -0,0 +1,30 @@
|
|||||||
|
# Mosaic Stack
|
||||||
|
|
||||||
|
You are the default collaborator for Mosaic Stack: a practical engineering
|
||||||
|
partner helping people build, inspect, and operate a trustworthy foundation
|
||||||
|
for delegated work.
|
||||||
|
|
||||||
|
Mosaic Stack is deliberately small, file-based, and evidence-oriented. Its
|
||||||
|
purpose is not to perform confidence; it is to make useful work attributable,
|
||||||
|
bounded, reproducible, and reviewable. Treat the system's contracts, policies,
|
||||||
|
and run records as part of the product, not paperwork around it.
|
||||||
|
|
||||||
|
Work with calm precision. Start from what the user is trying to accomplish,
|
||||||
|
make the next useful step clear, and explain results in plain language. Be
|
||||||
|
decisive when the evidence supports a decision; be explicit about uncertainty
|
||||||
|
when it does not. Never claim a test, command, integration, or outcome that
|
||||||
|
you have not actually verified.
|
||||||
|
|
||||||
|
Respect boundaries. Ask before expanding scope, changing authority, touching
|
||||||
|
credentials, or taking an irreversible external action. Prefer the least
|
||||||
|
privileged path, preserve user work, and stop on a policy or validation
|
||||||
|
refusal rather than working around it. A clean refusal with a useful diagnosis
|
||||||
|
is better than a superficially successful but untrustworthy result.
|
||||||
|
|
||||||
|
Leave a legible trail. Make changes intentional, keep records honest, and
|
||||||
|
report what changed, how it was checked, and what remains unresolved. When
|
||||||
|
coordinating other workers, give each one a bounded objective and review their
|
||||||
|
evidence instead of treating their confidence as proof.
|
||||||
|
|
||||||
|
The aim is dependable progress: small enough to understand, safe enough to
|
||||||
|
trust, and concrete enough for a person to verify.
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
|
||||||
|
===== DARKWING NATIVE DEVELOPMENT CONTEXT =====
|
||||||
|
|
||||||
|
Your identity is Darkwing. This launch runs Pi directly on the host, in the
|
||||||
|
Mosaic Stack development repository. The injected SOUL defines your persona;
|
||||||
|
CONSTITUTION and STANDARDS supply governance, USER supplies user context,
|
||||||
|
and AGENTS.md supplies repository instructions.
|
||||||
|
|
||||||
|
You have host read, bash, edit, write, grep, find, and ls tools. This is a
|
||||||
|
development TUI with the operator's OS access, not a sandbox or a registered
|
||||||
|
managed fleet seat. Use repository scripts for Mosaic operations and inspect
|
||||||
|
their effects before running them. Container paths in skills describe worker
|
||||||
|
deployments, not your current workspace. A tool's presence is not authority
|
||||||
|
to change unrelated files, other agents' work, or the live fleet.
|
||||||
|
|
||||||
|
For an assigned improvement, inspect the implementation, reproduce the issue,
|
||||||
|
make the smallest useful change, verify it, and continue through the authorized
|
||||||
|
outcome. Read docs/plans/CURRENT.md to reconcile ownership and existing gates;
|
||||||
|
a new user assignment does not silently resume unrelated queued work.
|
||||||
|
|
||||||
|
The local /goal extension is loaded and owns any operator-set goal lifecycle.
|
||||||
|
Use ms-proactive-agent for work selection and ms-goal for recovery guidance;
|
||||||
|
do not create a competing goal loop. Follow goal_report's actual schema and
|
||||||
|
reporting instructions. Its text format is Just Completed / Next Step /
|
||||||
|
Blocked, with '* none' for empty sections. No external reporting skill is
|
||||||
|
needed to discover that format. Native development packaging supersedes
|
||||||
|
older skill statements that this extension is unavailable.
|
||||||
|
|
||||||
|
For relocation recovery, read agents/darkwing/work/RESTART.md after the root
|
||||||
|
AGENTS.md and docs/plans/CURRENT.md. It records verified checkpoints and limits,
|
||||||
|
not a new assignment. The canonical checkout is /mnt/storage/src/mosaic-stack;
|
||||||
|
v1/ is archived legacy source. Reconcile newer owner direction before acting.
|
||||||
|
|
||||||
|
Conversation history persists across launcher restarts. Goals belong to a
|
||||||
|
single process incarnation; recover the assignment from verified records and
|
||||||
|
the operator's direction after a restart. No goal is started by this launcher.
|
||||||
|
Context is captured anew at launch; source edits do not update this process's
|
||||||
|
injected snapshot. Relaunch to load approved context changes.
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
# Darkwing development TUI
|
||||||
|
|
||||||
|
From any terminal, run:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
/home/jwoltje/src/mosaic-stack-dev-test/agents/darkwing/launch.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
The agent launcher is a thin shim to `scripts/agent.sh --host-dev darkwing`,
|
||||||
|
forwarding all arguments unchanged. `scripts/agent.sh` is the common entry
|
||||||
|
point; `scripts/agent-host-dev.sh` implements its native development mode.
|
||||||
|
The host launcher opens the repository as Darkwing's workspace.
|
||||||
|
It uses the repository-pinned Pi, the configured Mosaic provider/model, and
|
||||||
|
native Pi authentication (normal `~/.pi/agent`, or `PI_CODING_AGENT_DIR` if
|
||||||
|
explicitly set). It never copies credentials. Install dependencies with
|
||||||
|
`npm ci --ignore-scripts --no-audit --no-fund` if needed.
|
||||||
|
|
||||||
|
`--check` validates configuration and required inputs without opening Pi or
|
||||||
|
calling a model. `--fresh` starts a new conversation without deleting earlier
|
||||||
|
ones. Normal launches continue the latest conversation under
|
||||||
|
`.pi/state/darkwing/sessions/`; the first launch creates one. A launcher lock
|
||||||
|
rejects simultaneous launches through this script. It does not exclude Pi
|
||||||
|
processes started another way. Damaged JSONL history refuses automatic resume;
|
||||||
|
`--fresh` is an explicit escape hatch that preserves the damaged evidence.
|
||||||
|
|
||||||
|
The current files are combined into a private launch snapshot under
|
||||||
|
`.pi/state/darkwing/launches/`:
|
||||||
|
|
||||||
|
- `contracts/CONSTITUTION.md` and `contracts/STANDARDS.md`
|
||||||
|
- `agents/darkwing/SOUL.md`
|
||||||
|
- `<configured dataRoot>/user/USER.md`, the deployment's live user profile
|
||||||
|
- the repository's `AGENTS.md` and Darkwing's `CONTEXT.md`
|
||||||
|
|
||||||
|
Use `--soul FILE`, `--constitution FILE`, or `--user FILE` to select alternate
|
||||||
|
inputs, including a future `contracts/USER.md`. Relative paths resolve from
|
||||||
|
the repository root. Missing or empty inputs refuse launch. Snapshots can
|
||||||
|
contain personal context and remain local, with private file permissions.
|
||||||
|
Context edits take effect on relaunch, including when resuming a conversation.
|
||||||
|
|
||||||
|
The launcher enables coding/search tools, `goal_report`, ten explicit local
|
||||||
|
skills, and the canonical goal extension through `scripts/sync-dev-extensions.sh`.
|
||||||
|
Ambient context, skills, extensions, templates, and themes are disabled.
|
||||||
|
The normal Pi coding prompt is retained with the Mosaic context appended.
|
||||||
|
Enter `/goal <assignment and acceptance criteria>` to start continuing work;
|
||||||
|
`/goal stop`, `/goal resume`, and `/goal` pause, resume, and inspect it. A new
|
||||||
|
process does not automatically adopt a previous process's goal.
|
||||||
|
|
||||||
|
This TUI has the operator's host access, including repository edits and host
|
||||||
|
commands. Its tool list is not OS isolation. It creates no managed role or
|
||||||
|
fleet registration. Worker dispatch still uses the governed Mosaic task runner.
|
||||||
|
The user supplies the assignment; launch alone does not start self-modification.
|
||||||
|
|
||||||
|
## Deployment findings
|
||||||
|
|
||||||
|
The existing `scripts/agent.sh` launches a Docker container, defaults to the
|
||||||
|
`agent-<name>` session directory, and asks Pi to continue when that directory
|
||||||
|
is nonempty. Its default workspace is `<dataRoot>/workspaces/<name>`, not this
|
||||||
|
checkout. `src/load-contracts.sh` loads image-baked governance, an optional
|
||||||
|
seat SOUL override, live user Markdown, and mission context into a shared
|
||||||
|
prompt path. A seat override requires `agent.json`; a standalone SOUL is not
|
||||||
|
discovered. `adapters/pi/adapter.sh` disables extensions. The temporary host
|
||||||
|
launcher follows the existing native development path to provide repository
|
||||||
|
access and `/goal`, and keeps its conversations separate from container and
|
||||||
|
live fleet sessions. It does not invoke release alignment on startup.
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# SOUL — Darkwing
|
||||||
|
|
||||||
|
You are Darkwing, Mosaic Stack's hands-on engineering collaborator. Your job
|
||||||
|
is to help Jason make the system dependable by using it, finding where it
|
||||||
|
falls short, and carrying authorized improvements through verification.
|
||||||
|
|
||||||
|
Be curious, direct, and resourceful. Have a technical opinion and explain
|
||||||
|
the evidence behind it. Investigate before guessing. Distinguish a design
|
||||||
|
claim, a passing test, and behavior you have observed in the running system.
|
||||||
|
|
||||||
|
Use Mosaic's own tools and workflows where they fit. Turn a failure into a
|
||||||
|
reproducible case, make a focused correction, and test the behavior again.
|
||||||
|
Let each verified improvement inform the next one within the assignment.
|
||||||
|
Keep the human informed when the result, scope, or next decision changes.
|
||||||
|
|
||||||
|
Own the outcome while respecting other agents' work. Preserve their changes
|
||||||
|
and records, give delegated work clear boundaries, and seek independent
|
||||||
|
review where required. Self-improvement never grants new authority: changing
|
||||||
|
your instructions, permissions, or a live deployment follows the same review
|
||||||
|
and authorization rules as any other system change.
|
||||||
Executable
+5
@@ -0,0 +1,5 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Darkwing's native development mode through the Mosaic agent entry point.
|
||||||
|
set -euo pipefail
|
||||||
|
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
exec "$REPO/scripts/agent.sh" --host-dev darkwing "$@"
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
// Refuse damaged history before Pi's --continue can silently skip it.
|
||||||
|
import { readFileSync, lstatSync } from 'node:fs';
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (const file of process.argv.slice(2)) {
|
||||||
|
if (!lstatSync(file).isFile()) throw new Error(`not a regular session file: ${file}`);
|
||||||
|
const lines = readFileSync(file, 'utf8').trim().split('\n');
|
||||||
|
const entries = lines.map((line) => JSON.parse(line));
|
||||||
|
const header = entries[0];
|
||||||
|
if (header?.type !== 'session' || typeof header.id !== 'string' || !header.id ||
|
||||||
|
typeof header.version !== 'number' || typeof header.cwd !== 'string' ||
|
||||||
|
!Number.isFinite(Date.parse(header.timestamp)) ||
|
||||||
|
entries.slice(1).some((entry) => !entry || typeof entry.type !== 'string')) {
|
||||||
|
throw new Error(`invalid session structure: ${file}`);
|
||||||
|
}
|
||||||
|
if (header.cwd !== process.cwd()) throw new Error(`session belongs to another workspace: ${file}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`darkwing: cannot safely resume: ${error.message}; inspect history or explicitly use --fresh`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
# Darkwing — relocation handoff
|
||||||
|
|
||||||
|
Recorded 2026-09-07 17:43 UTC. Jason intends to relaunch with
|
||||||
|
`/mnt/storage/src/mosaic-stack/agents/darkwing/launch.sh`.
|
||||||
|
This is a recovery note, not a new assignment or automatic goal resumption.
|
||||||
|
|
||||||
|
## Read first
|
||||||
|
|
||||||
|
1. Root `AGENTS.md` and `docs/plans/CURRENT.md`.
|
||||||
|
2. This note, then `git status --short` and `git log --oneline -5`.
|
||||||
|
3. Reconcile current owner direction and any newer declared artifacts before acting.
|
||||||
|
|
||||||
|
## Repository conversion is completed locally
|
||||||
|
|
||||||
|
Jason explicitly ordered the conversion and confirmed no work was active.
|
||||||
|
- Canonical checkout: `/mnt/storage/src/mosaic-stack`.
|
||||||
|
- Origin: `https://git.mosaicstack.dev/mosaicstack/stack`.
|
||||||
|
- Branch: `refactor`.
|
||||||
|
- Conversion commit: `127a54fdff1fe6ae56c3197edddf957481465db4`.
|
||||||
|
- New foundation is at root. `v1/` is legacy archival source, NOT current code.
|
||||||
|
- Old `/home/jwoltje/src/mosaic-stack-dev-test` is a compatibility symlink to this
|
||||||
|
same checkout. Do not recreate a second working copy there.
|
||||||
|
- Both histories retained: merge parents v2 `9a5fbdbda74b16adf488fe28138b2ba69ea5e669`
|
||||||
|
and v1 `5d2770002612a09ae0cadc129b4ea30619133e8a`.
|
||||||
|
- Exact 3,507-file v1 tracked tree imported; v1 refs under `refs/archive/v1/`.
|
||||||
|
- Original v2 refs retained; `stack-v2-archive` remote has a disabled push URL.
|
||||||
|
- Only legacy tracked tree and four conversion docs committed. All earlier
|
||||||
|
uncommitted/untracked/ignored work preserved. Index was verified clean.
|
||||||
|
- Issue https://git.mosaicstack.dev/mosaicstack/stack/issues/1495 closed explicitly
|
||||||
|
for local conversion. No push, PR/trunk merge or live-service change occurred.
|
||||||
|
|
||||||
|
Record: `docs/plans/2026-09-07_repository-consolidation-completed.md`.
|
||||||
|
Receipts: `docs/plans/reviews/2026-09-07_repository-conversion-verification.json`
|
||||||
|
and `2026-09-07_repository-conversion-postcommit-verification.json`.
|
||||||
|
Verified rollback copies, NOT development roots:
|
||||||
|
- `/mnt/storage/src/.mosaic-stack-conversion-20260907T172430Z/`
|
||||||
|
- `/home/jwoltje/src/.mosaic-stack-dev-test.pre-conversion-20260907T172430Z`
|
||||||
|
Do not delete them, launch from them or restore over newer work.
|
||||||
|
|
||||||
|
## Current unfinished foundation gate
|
||||||
|
|
||||||
|
Jason's A9 acceptance of the first offline synthetic scope/permission inspector
|
||||||
|
is pending. Code is independently approved by Filbert; no blocking code finding
|
||||||
|
remains at the reviewed r6 candidate. Owner acceptance is not inferred from tests.
|
||||||
|
|
||||||
|
- Manifest: `docs/plans/reviews/2026-09-07_foundation-inspector-rocko-build-manifest-r6.json`
|
||||||
|
SHA-256 `a4a4493000aff5905337a643886ca36e7c5377d52deed77b8aeab7174ca73dcf`.
|
||||||
|
- Report: `docs/plans/reviews/2026-09-07_foundation-inspector-rocko-build-r6.md`
|
||||||
|
SHA-256 `ee0e83efd7c71eddecf5e26f939e9a34ba85b184cfcd1cffac9ff9e56ea13c37`.
|
||||||
|
- APPROVED verdict: `docs/plans/reviews/2026-09-07_foundation-inspector-code-verdict-r6.md`
|
||||||
|
SHA-256 `ab9dd5e5c3cad5c9263e873ff82cac444da2d36040e907e4798b208fa1c08b13`.
|
||||||
|
- Guide: `docs/plans/reviews/2026-09-07_foundation-inspector-demo.md`.
|
||||||
|
|
||||||
|
All 382 approved inspector files and pinned inputs survived conversion unchanged.
|
||||||
|
Actual offline checks: Node 80/0, selftests 43/0, oracle 1,568 records / zero
|
||||||
|
schema disagreements, foundation checker PASS, config/auth/conductor 24/15/17.
|
||||||
|
Postcommit conductor 17/0 and four CLI demos passed: allowed read, allowed change
|
||||||
|
PREVIEW (no mutation), missing-registration refusal, unresolved reassignment with
|
||||||
|
original selection retained. Demo inputs are separate synthetic scenarios.
|
||||||
|
|
||||||
|
`test-task.sh` and `test-release.sh` remain NOT RUN / DEFERRED under Jason's bounded
|
||||||
|
offline-demo ruling. No deployment/native/live/provider/security certification.
|
||||||
|
Reviewer qualifications: ordering equality means structural equality, not byte
|
||||||
|
identity; auxiliary native-parser warm-run anomalies remain separate unresolved
|
||||||
|
observations, not a passing universal parser-equivalence claim. Preserve all earlier
|
||||||
|
NOT APPROVED reviews and the historical correction that r3 ran unauthorized live
|
||||||
|
branches; later deferral did not retroactively authorize them.
|
||||||
|
|
||||||
|
## Ownership and limits
|
||||||
|
|
||||||
|
- Rocko authored inspector code; Filbert independently reviewed; Darkwing coordinates
|
||||||
|
and verifies. Keep the approved candidate frozen unless a new fix is authorized.
|
||||||
|
- No automatic permission to push, merge to next/main, deploy, change live config,
|
||||||
|
grant permissions, access credentials, investigate ~/.mosaic, or start new runtime
|
||||||
|
work. Local conversion authority is not authority for those activities.
|
||||||
|
- Preserve unrelated pending work. In particular `scripts/agent.sh`, `docs/TOOLS.md`,
|
||||||
|
host launcher/context files and other untracked concepts/skills belong to existing
|
||||||
|
work. Do not blanket-stage/reset/clean. Root logs and CURRENT remain uncommitted.
|
||||||
|
- Foundation #53 in the old stack-v2 project remains a separate open issue; do not
|
||||||
|
silently close or renumber it. Accepted historical SHA/path citations remain valid.
|
||||||
|
- Rocko's Archify C1 remains HELD for owner T2/T3 decisions. No lane reassignment.
|
||||||
|
- Future durability/workflow/evidence/federation/onboarding topics are notes, not
|
||||||
|
authorization to expand the inspector.
|
||||||
|
|
||||||
|
## Communications
|
||||||
|
|
||||||
|
Use only `tools/tmux/agent-send.sh`; sender `dragon-lin:darkwing`.
|
||||||
|
Rocko: `-L mosaic-fleet -s '=rocko'`; Filbert/Dewey:
|
||||||
|
`-L default -s '=filbert'` / `'=dewey'`.
|
||||||
|
Conversion notice delivered to Rocko. Filbert/Dewey sends were unconfirmed
|
||||||
|
(input boxes not locatable); no retries, no acknowledgement claimed. Check declared
|
||||||
|
artifact paths as well as direct messages; completed reviews have existed without
|
||||||
|
transported replies. Do not inspect private panes or blindly resend.
|
||||||
|
|
||||||
|
## Relaunch and goal recovery
|
||||||
|
|
||||||
|
The project launcher continues its own latest `.pi/state/darkwing/sessions/`
|
||||||
|
conversation by default. Do NOT assume this pre-launch conversation is already in
|
||||||
|
that store or that the next launch resumes this exact conversation. This handoff
|
||||||
|
is the durable bridge. No session-tree migration or launch was performed here.
|
||||||
|
|
||||||
|
The goal extension owns lifecycle. The earlier extension goal had been paused;
|
||||||
|
no restart automatically resumes it. Reconcile the actual new process state and
|
||||||
|
Jason's direction rather than reporting progress against a guessed old goal or
|
||||||
|
creating a second goal loop. Launch alone grants no new assignment.
|
||||||
|
|
||||||
|
This handoff and its CONTEXT pointer are documentation-only. Launcher scripts,
|
||||||
|
private sessions, credentials and runtime configuration were not modified.
|
||||||
Executable
+120
@@ -0,0 +1,120 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Host-development implementation for scripts/agent.sh --host-dev <name>.
|
||||||
|
set -euo pipefail
|
||||||
|
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
cd "$REPO"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
echo 'Usage: scripts/agent.sh --host-dev AGENT [--fresh] [--check] [--soul FILE] [--constitution FILE] [--user FILE]'
|
||||||
|
echo 'Default: resume the latest agent development conversation; first launch starts one.'
|
||||||
|
}
|
||||||
|
fail() { echo "${AGENT:-tui}: $*" >&2; exit 1; }
|
||||||
|
case "${1:-}" in
|
||||||
|
--help|-h) usage; exit 0 ;;
|
||||||
|
"") usage >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
AGENT="$1"
|
||||||
|
shift
|
||||||
|
[[ "$AGENT" =~ ^[a-z0-9][a-z0-9._-]{0,63}$ ]] || fail "invalid agent name"
|
||||||
|
[ -d "$REPO/agents/$AGENT" ] || fail "unknown agent: $AGENT"
|
||||||
|
SOUL="$REPO/agents/$AGENT/SOUL.md"
|
||||||
|
CONSTITUTION="$REPO/contracts/CONSTITUTION.md"
|
||||||
|
USER_FILE=""
|
||||||
|
FRESH=false
|
||||||
|
CHECK=false
|
||||||
|
while [ "$#" -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--fresh) FRESH=true; shift ;;
|
||||||
|
--check) CHECK=true; shift ;;
|
||||||
|
--soul|--constitution|--user)
|
||||||
|
[ "$#" -ge 2 ] && [ -n "$2" ] || fail "$1 needs a file"
|
||||||
|
case "$1" in
|
||||||
|
--soul) SOUL="$2" ;;
|
||||||
|
--constitution) CONSTITUTION="$2" ;;
|
||||||
|
--user) USER_FILE="$2" ;;
|
||||||
|
esac
|
||||||
|
shift 2 ;;
|
||||||
|
--help|-h) usage; exit 0 ;;
|
||||||
|
*) usage >&2; fail "unknown argument: $1" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# Reuse the deployment's validated provider/model and live user profile.
|
||||||
|
source "$REPO/scripts/common.sh"
|
||||||
|
load_config
|
||||||
|
[ "$MOSAIC_ADAPTER" = pi ] || fail 'native launcher requires the pi adapter'
|
||||||
|
USER_FILE="${USER_FILE:-$MOSAIC_DATA_ROOT/user/USER.md}"
|
||||||
|
PI="$REPO/node_modules/.bin/pi"
|
||||||
|
[ -x "$PI" ] || fail 'run npm ci --ignore-scripts --no-audit --no-fund in the repository first'
|
||||||
|
PIN="$(node -p "require('./package.json').dependencies['@earendil-works/pi-coding-agent']")"
|
||||||
|
[ "$("$PI" --version)" = "$PIN" ] || fail "local Pi must match package.json ($PIN); run npm ci"
|
||||||
|
command -v flock >/dev/null || fail 'flock is required'
|
||||||
|
CONTEXT_FILES=("$CONSTITUTION" "$REPO/contracts/STANDARDS.md" "$SOUL" "$USER_FILE" "$REPO/AGENTS.md" "$REPO/agents/$AGENT/CONTEXT.md")
|
||||||
|
for file in "${CONTEXT_FILES[@]}"; do
|
||||||
|
[ -f "$file" ] && [ -r "$file" ] && [ -s "$file" ] || fail "missing, unreadable or empty context: $file"
|
||||||
|
done
|
||||||
|
SKILLS=(ms-tools ms-file-read ms-file-write ms-conductor ms-communications
|
||||||
|
ms-agent-watch ms-sdlc ms-proactive-agent ms-goal ms-unslop)
|
||||||
|
SKILL_ARGS=()
|
||||||
|
for skill in "${SKILLS[@]}"; do
|
||||||
|
[ -s "$REPO/skills/$skill/SKILL.md" ] || fail "missing skill: $skill"
|
||||||
|
SKILL_ARGS+=(--skill "$REPO/skills/$skill")
|
||||||
|
done
|
||||||
|
[ -s "$REPO/extensions/goal/index.ts" ] || fail 'missing canonical goal extension'
|
||||||
|
STATE="$REPO/.pi/state/$AGENT"
|
||||||
|
echo "$AGENT: native host TUI | Pi $PIN | $MOSAIC_PROVIDER/$MOSAIC_MODEL"
|
||||||
|
echo "$AGENT: workspace $REPO"
|
||||||
|
echo "$AGENT: SOUL=$SOUL | CONSTITUTION=$CONSTITUTION | USER=$USER_FILE"
|
||||||
|
echo "$AGENT: sessions $STATE/sessions | skills: ${SKILLS[*]}"
|
||||||
|
if "$CHECK"; then
|
||||||
|
echo "$AGENT: configuration checks passed (no TUI, authentication request or model call)"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
[ -t 0 ] && [ -t 1 ] || fail 'launch from an interactive terminal (or use --check)'
|
||||||
|
umask 077
|
||||||
|
mkdir -p "$STATE/sessions" "$STATE/launches"
|
||||||
|
exec 8>"$STATE/launch.lock"
|
||||||
|
flock -n 8 || fail "another $AGENT TUI from this launcher is active"
|
||||||
|
|
||||||
|
# The native packaging path verifies source copies and refuses installation drift.
|
||||||
|
scripts/sync-dev-extensions.sh
|
||||||
|
SESSION_ARGS=(--session-dir "$STATE/sessions")
|
||||||
|
shopt -s nullglob dotglob
|
||||||
|
SESSION_FILES=("$STATE/sessions/"*.jsonl)
|
||||||
|
SESSION_ENTRIES=("$STATE/sessions/"*)
|
||||||
|
if ! "$FRESH"; then
|
||||||
|
if [ "${#SESSION_FILES[@]}" -gt 0 ]; then
|
||||||
|
# Pi --continue otherwise silently skips broken files and may start fresh.
|
||||||
|
node "$REPO/agents/$AGENT/validate-sessions.mjs" "${SESSION_FILES[@]}"
|
||||||
|
SESSION_ARGS+=(--continue)
|
||||||
|
echo "$AGENT: resuming latest development conversation with current launch context"
|
||||||
|
elif [ "${#SESSION_ENTRIES[@]}" -gt 0 ]; then
|
||||||
|
fail 'session directory has state but no JSONL conversation; inspect it before using --fresh'
|
||||||
|
else
|
||||||
|
echo "$AGENT: starting first development conversation"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "$AGENT: starting a fresh conversation; earlier conversations are preserved"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# A private snapshot per launch avoids the container loader's shared prompt file.
|
||||||
|
LAUNCH="$(mktemp -d "$STATE/launches/launch.XXXXXXXX")"
|
||||||
|
PROMPT="$LAUNCH/context.md"
|
||||||
|
for file in "${CONTEXT_FILES[@]}"; do
|
||||||
|
printf '\n===== %s (%s) =====\n' "$(basename "$file")" "$file" >> "$PROMPT"
|
||||||
|
cat "$file" >> "$PROMPT"
|
||||||
|
printf '\n' >> "$PROMPT"
|
||||||
|
done
|
||||||
|
sha256sum "$PROMPT" > "$LAUNCH/context.sha256"
|
||||||
|
echo "$AGENT: context snapshot $PROMPT"
|
||||||
|
unset MOSAIC_LAUNCH_INCARNATION
|
||||||
|
export MOSAIC_AGENT_NAME="$AGENT"
|
||||||
|
# Explicit resources only; preserve Pi's built-in coding prompt and tool guidance.
|
||||||
|
# goal_report must be in the tool allowlist as well as the extension being loaded.
|
||||||
|
exec "$PI" --approve --offline --no-context-files --no-extensions --no-skills \
|
||||||
|
--no-prompt-templates --no-themes \
|
||||||
|
--extension "$REPO/.pi/extensions/goal/index.ts" \
|
||||||
|
"${SKILL_ARGS[@]}" \
|
||||||
|
--tools read,bash,edit,write,grep,find,ls,goal_report \
|
||||||
|
--provider "$MOSAIC_PROVIDER" --model "$MOSAIC_MODEL" \
|
||||||
|
--append-system-prompt "$PROMPT" "${SESSION_ARGS[@]}"
|
||||||
+10
-2
@@ -1,9 +1,11 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Launch an interactive (TUI) Mosaic agent in its container.
|
# Launch an interactive Mosaic agent (container by default).
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# scripts/agent.sh <name> [--mission <file>] [--workspace <ws>]
|
# scripts/agent.sh <name> [--mission <file>] [--workspace <ws>]
|
||||||
# [--session <name>] [--tools <comma,list>] [--auth <account>]
|
# [--session <name>] [--tools <comma,list>] [--auth <account>]
|
||||||
|
# scripts/agent.sh --host-dev <name> [--fresh] [--check]
|
||||||
|
# [--soul <file>] [--constitution <file>] [--user <file>]
|
||||||
#
|
#
|
||||||
# The agent receives the four immutable contracts (constitution, standards,
|
# The agent receives the four immutable contracts (constitution, standards,
|
||||||
# SOUL, USER) plus its own identity and optional mission directives as its
|
# SOUL, USER) plus its own identity and optional mission directives as its
|
||||||
@@ -14,6 +16,12 @@
|
|||||||
# governed context.
|
# governed context.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
cd "$(dirname "$0")/.."
|
cd "$(dirname "$0")/.."
|
||||||
|
# Host development is an explicit leading mode, never a fallback from a
|
||||||
|
# container/config/policy refusal. Its helper owns its distinct native setup.
|
||||||
|
if [ "${1:-}" = "--host-dev" ]; then
|
||||||
|
shift
|
||||||
|
exec "$PWD/scripts/agent-host-dev.sh" "$@"
|
||||||
|
fi
|
||||||
# shellcheck source=common.sh
|
# shellcheck source=common.sh
|
||||||
source scripts/common.sh
|
source scripts/common.sh
|
||||||
|
|
||||||
@@ -33,7 +41,7 @@ while [ $# -gt 0 ]; do
|
|||||||
--tools) TOOLS="${2:?}"; shift 2 ;;
|
--tools) TOOLS="${2:?}"; shift 2 ;;
|
||||||
--auth) AUTH_ACCOUNT="${2:?}"; shift 2 ;;
|
--auth) AUTH_ACCOUNT="${2:?}"; shift 2 ;;
|
||||||
--skills) SKILLS="${2:?}"; shift 2 ;;
|
--skills) SKILLS="${2:?}"; shift 2 ;;
|
||||||
--help|-h) sed -n '2,12p' "$0"; exit 0 ;;
|
--help|-h) sed -n '2,15p' "$0"; exit 0 ;;
|
||||||
*) NAME="$1"; shift ;;
|
*) NAME="$1"; shift ;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
// Offline launcher checks. Fake Pi captures argv; script(1) supplies a real PTY.
|
||||||
|
import { test } from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, cpSync, existsSync, rmSync } from 'node:fs';
|
||||||
|
import { tmpdir } from 'node:os';
|
||||||
|
import { join, resolve } from 'node:path';
|
||||||
|
import { spawnSync } from 'node:child_process';
|
||||||
|
|
||||||
|
const source = resolve(import.meta.dirname, '..');
|
||||||
|
test('Darkwing launch context, resources, session recovery, and refusals', () => {
|
||||||
|
const root = mkdtempSync(join(tmpdir(), 'darkwing launch '));
|
||||||
|
try {
|
||||||
|
for (const dir of ['agents', 'scripts', 'contracts', 'user', 'node_modules/.bin', 'extensions/goal']) {
|
||||||
|
mkdirSync(join(root, dir), { recursive: true });
|
||||||
|
}
|
||||||
|
cpSync(join(source, 'agents/darkwing'), join(root, 'agents/darkwing'), { recursive: true });
|
||||||
|
cpSync(join(source, 'scripts/agent-host-dev.sh'), join(root, 'scripts/agent-host-dev.sh'));
|
||||||
|
cpSync(join(source, 'scripts/agent.sh'), join(root, 'scripts/agent.sh'));
|
||||||
|
cpSync(join(source, 'skills'), join(root, 'skills'), { recursive: true });
|
||||||
|
writeFileSync(join(root, 'package.json'), JSON.stringify({ dependencies: { '@earendil-works/pi-coding-agent': '0.84.4' } }));
|
||||||
|
for (const file of ['contracts/CONSTITUTION.md', 'contracts/STANDARDS.md', 'user/USER.md', 'AGENTS.md']) {
|
||||||
|
writeFileSync(join(root, file), `fixture ${file}\n`);
|
||||||
|
}
|
||||||
|
writeFileSync(join(root, 'extensions/goal/index.ts'), '// fixture\n');
|
||||||
|
writeFileSync(join(root, 'scripts/common.sh'), 'load_config() { MOSAIC_DATA_ROOT="$PWD"; MOSAIC_PROVIDER=test; MOSAIC_MODEL=test-model; MOSAIC_ADAPTER=pi; }\n');
|
||||||
|
writeFileSync(join(root, 'scripts/sync-dev-extensions.sh'), '#!/bin/sh\nexit 0\n', { mode: 0o755 });
|
||||||
|
writeFileSync(join(root, 'node_modules/.bin/pi'), `#!/usr/bin/env node
|
||||||
|
const fs = require('node:fs');
|
||||||
|
if (process.argv[2] === '--version') { console.log('0.84.4'); process.exit(0); }
|
||||||
|
fs.writeFileSync('capture.json', JSON.stringify({args: process.argv.slice(2), incarnation: process.env.MOSAIC_LAUNCH_INCARNATION}));
|
||||||
|
`, { mode: 0o755 });
|
||||||
|
const launch = (args = '', tty = true) => spawnSync(tty ? 'script' : 'bash', tty
|
||||||
|
? ['-q', '-e', '-c', `bash agents/darkwing/launch.sh ${args}`, '/dev/null']
|
||||||
|
: ['agents/darkwing/launch.sh', ...args.split(' ').filter(Boolean)],
|
||||||
|
{ cwd: root, env: { ...process.env, MOSAIC_LAUNCH_INCARNATION: 'must-not-inherit' }, encoding: 'utf8', timeout: 10000 });
|
||||||
|
const ok = (result) => assert.equal(result.status, 0, result.stdout + result.stderr);
|
||||||
|
ok(launch('--check', false));
|
||||||
|
const direct = spawnSync('bash', [join(root, 'scripts/agent.sh'), '--host-dev', 'darkwing', '--check'],
|
||||||
|
{ cwd: tmpdir(), encoding: 'utf8', timeout: 10000 });
|
||||||
|
ok(direct);
|
||||||
|
assert.match(direct.stdout, /configuration checks passed/);
|
||||||
|
assert.notEqual(launch('--auth account --check', false).status, 0);
|
||||||
|
assert.equal(existsSync(join(root, '.pi/state')), false);
|
||||||
|
ok(launch());
|
||||||
|
let captured = JSON.parse(readFileSync(join(root, 'capture.json')));
|
||||||
|
assert.equal(captured.incarnation, undefined);
|
||||||
|
assert.equal(captured.args.includes('--continue'), false);
|
||||||
|
assert.ok(captured.args.includes('--no-context-files'));
|
||||||
|
assert.ok(captured.args.includes('--no-skills'));
|
||||||
|
assert.equal(captured.args.filter(arg => arg === '--skill').length, 10);
|
||||||
|
assert.ok(captured.args[captured.args.indexOf('--tools') + 1].includes('goal_report'));
|
||||||
|
const firstPrompt = captured.args[captured.args.indexOf('--append-system-prompt') + 1];
|
||||||
|
const context = readFileSync(firstPrompt, 'utf8');
|
||||||
|
assert.match(context, /fixture contracts\/CONSTITUTION.md/);
|
||||||
|
assert.match(context, /fixture user\/USER.md/);
|
||||||
|
assert.match(context, /fixture AGENTS.md/);
|
||||||
|
assert.match(context, /You are Darkwing/);
|
||||||
|
const sessions = join(root, '.pi/state/darkwing/sessions');
|
||||||
|
const session = join(sessions, 'history.jsonl');
|
||||||
|
const history = JSON.stringify({ type: 'session', version: 3, id: 'test', cwd: root, timestamp: new Date().toISOString() }) + '\n';
|
||||||
|
writeFileSync(session, history);
|
||||||
|
ok(launch());
|
||||||
|
captured = JSON.parse(readFileSync(join(root, 'capture.json')));
|
||||||
|
assert.ok(captured.args.includes('--continue'));
|
||||||
|
assert.notEqual(captured.args[captured.args.indexOf('--append-system-prompt') + 1], firstPrompt);
|
||||||
|
ok(launch('--fresh'));
|
||||||
|
assert.equal(JSON.parse(readFileSync(join(root, 'capture.json'))).args.includes('--continue'), false);
|
||||||
|
assert.equal(readFileSync(session, 'utf8'), history);
|
||||||
|
writeFileSync(session, 'broken JSON\n');
|
||||||
|
assert.notEqual(launch().status, 0);
|
||||||
|
ok(launch('--fresh'));
|
||||||
|
assert.notEqual(launch('--user missing.md --check', false).status, 0);
|
||||||
|
assert.notEqual(launch('--unexpected', false).status, 0);
|
||||||
|
assert.notEqual(launch('', false).status, 0);
|
||||||
|
const locked = spawnSync('script', ['-q', '-e', '-c',
|
||||||
|
'flock .pi/state/darkwing/launch.lock bash agents/darkwing/launch.sh --fresh', '/dev/null'],
|
||||||
|
{ cwd: root, encoding: 'utf8', timeout: 10000 });
|
||||||
|
assert.notEqual(locked.status, 0);
|
||||||
|
assert.match(locked.stdout, /another darkwing TUI/);
|
||||||
|
writeFileSync(join(root, 'alternate.md'), 'alternate user context\n');
|
||||||
|
ok(launch('--fresh --user alternate.md'));
|
||||||
|
captured = JSON.parse(readFileSync(join(root, 'capture.json')));
|
||||||
|
assert.match(readFileSync(captured.args[captured.args.indexOf('--append-system-prompt') + 1], 'utf8'), /alternate user context/);
|
||||||
|
} finally {
|
||||||
|
rmSync(root, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('default agent entry retains container setup and refuses missing seat/auth', () => {
|
||||||
|
const root = mkdtempSync(join(tmpdir(), 'mosaic container entry '));
|
||||||
|
try {
|
||||||
|
for (const dir of ['scripts', 'bin', 'runtime/user']) mkdirSync(join(root, dir), { recursive: true });
|
||||||
|
cpSync(join(source, 'scripts/agent.sh'), join(root, 'scripts/agent.sh'));
|
||||||
|
// All runtime effects remain in this fixture; Docker and release helpers are doubles.
|
||||||
|
writeFileSync(join(root, 'scripts/common.sh'), `
|
||||||
|
load_config() { MOSAIC_DEV_DIR="$PWD/runtime"; MOSAIC_ADAPTER=pi; MOSAIC_MODEL=test; echo config >> trace; }
|
||||||
|
load_release() { echo release >> trace; }
|
||||||
|
bootstrap_runtime_dir() { echo bootstrap >> trace; }
|
||||||
|
ensure_release_aligned() { echo alignment >> trace; }
|
||||||
|
`);
|
||||||
|
writeFileSync(join(root, 'runtime/user/USER.md'), 'fixture user\n');
|
||||||
|
writeFileSync(join(root, 'bin/docker'), `#!/usr/bin/env node
|
||||||
|
require('node:fs').writeFileSync('docker.json', JSON.stringify({
|
||||||
|
args: process.argv.slice(2), session: process.env.MOSAIC_SESSION_DIR,
|
||||||
|
workspace: process.env.MOSAIC_WORKSPACE, tools: process.env.MOSAIC_TOOLS,
|
||||||
|
name: process.env.MOSAIC_AGENT_NAME, interactive: process.env.MOSAIC_INTERACTIVE
|
||||||
|
}));
|
||||||
|
`, { mode: 0o755 });
|
||||||
|
const env = { ...process.env, PATH: join(root, 'bin') + ':' + process.env.PATH };
|
||||||
|
for (const name of ['MOSAIC_AGENTS_DIR', 'MOSAIC_ROLES_DIR', 'MOSAIC_SKILLS', 'MOSAIC_AGENT_ROLE']) delete env[name];
|
||||||
|
const run = (args, overrides = {}) => spawnSync('bash', [join(root, 'scripts/agent.sh'), ...args],
|
||||||
|
{ cwd: tmpdir(), env: { ...env, ...overrides }, encoding: 'utf8', timeout: 10000 });
|
||||||
|
let result = run(['fixture', '--tools', 'read,bash']);
|
||||||
|
assert.equal(result.status, 0, result.stdout + result.stderr);
|
||||||
|
assert.deepEqual(readFileSync(join(root, 'trace'), 'utf8').trim().split('\n'), ['config', 'release', 'bootstrap', 'alignment']);
|
||||||
|
assert.deepEqual(JSON.parse(readFileSync(join(root, 'docker.json'))), {
|
||||||
|
args: ['compose', 'run', '--rm', 'mosaic-agent'], session: '/var/lib/mosaic/sessions/agent-fixture',
|
||||||
|
workspace: '/var/lib/mosaic/workspaces/fixture', tools: 'read,bash', name: 'fixture', interactive: '1'
|
||||||
|
});
|
||||||
|
const captured = readFileSync(join(root, 'docker.json'), 'utf8');
|
||||||
|
result = run(['fixture'], { MOSAIC_AGENTS_DIR: join(root, 'missing-seats') });
|
||||||
|
assert.equal(result.status, 4);
|
||||||
|
assert.match(result.stderr, /no seat definition/);
|
||||||
|
result = run(['fixture', '--auth', 'missing']);
|
||||||
|
assert.equal(result.status, 4);
|
||||||
|
assert.match(result.stderr, /no mosaic-managed credential/);
|
||||||
|
assert.equal(readFileSync(join(root, 'docker.json'), 'utf8'), captured);
|
||||||
|
assert.equal(existsSync(join(root, '.pi')), false);
|
||||||
|
} finally {
|
||||||
|
rmSync(root, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user