mirror of
https://github.com/qwibitai/nanoclaw.git
synced 2026-07-06 18:52:03 +08:00
feat(skills): effect:restart directive — phasing is just directive order
The install→restart→wire "phases" aren't a driver concern; they're directives in document order. Restart is a run, placed between the install/credentials and the wiring, and the engine already runs directives top-to-bottom — so no phase orchestration is needed. - effect:restart: a run whose body restarts the service and waits for the ncl socket so a following wiring directive doesn't race it. New helper setup/lib/restart.sh (platform-aware restart + best-effort socket wait; the engine execs each body line separately, so one self-contained script). - ApplyOptions.skipEffects: run effects the CALLER owns and performs itself are skipped (not executed). A standalone /add-slack runs the restart; a headless rebuild or a setup that restarts once at the end passes ['restart'] and it's skipped. The declarative form of applyProviderSkill's isFlowOwnedCommand. add-slack now carries the restart between Credentials and the wire, so the whole flow is one self-contained ordered directive sequence. Dry-run: standalone runs the restart, a flow caller skips it — both fully apply, both wire. 604 pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -101,6 +101,15 @@ Set up event delivery (needs a public HTTPS URL for port 3000 — ngrok, a Cloud
|
||||
3. Interactivity & Shortcuts → toggle Interactivity on, set the same Request URL, Save Changes, then reinstall the app when Slack prompts.
|
||||
```
|
||||
|
||||
## Restart
|
||||
|
||||
Restart the service so it loads the Slack adapter and the credentials you just
|
||||
stored, and wait for its CLI socket before wiring:
|
||||
|
||||
```nc:run effect:restart
|
||||
bash setup/lib/restart.sh
|
||||
```
|
||||
|
||||
## Connect yourself
|
||||
|
||||
Wire your own Slack account as the owner so you can talk to the assistant, and
|
||||
|
||||
@@ -429,4 +429,17 @@ describe('programmatic apply via inputs', () => {
|
||||
expect(env).toContain('B=fromPrompter'); // prompter filled the gap
|
||||
expect(asked).toEqual(['b']); // 'a' was never asked — it came from inputs
|
||||
});
|
||||
|
||||
it('skipEffects skips a run the caller owns (effect:restart) but runs the rest', async () => {
|
||||
writeFileSync(
|
||||
join(pskill, 'SKILL.md'),
|
||||
'# restart demo\n\n```nc:run effect:build\npnpm run build\n```\n```nc:run effect:restart\nbash setup/lib/restart.sh\n```\n```nc:run effect:wire\nncl wire\n```\n',
|
||||
);
|
||||
const cmds: string[] = [];
|
||||
const res = await applySkill(pskill, proot, { inputs: {}, skipEffects: ['restart'], exec: (c) => void cmds.push(c) });
|
||||
expect(cmds).toContain('pnpm run build');
|
||||
expect(cmds).toContain('ncl wire');
|
||||
expect(cmds).not.toContain('bash setup/lib/restart.sh'); // restart owned by the caller → skipped
|
||||
expect(res.skipped.some((s) => /run restart: owned by the caller/.test(s))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -205,6 +205,10 @@ export interface ApplyOptions {
|
||||
// dep/run/branch-fetch; injectable for tests. Returns the command's stdout so
|
||||
// a `run capture:<var>` can bind it into a {{var}} (the twin of `prompt`).
|
||||
exec?: (cmd: string) => string | void | Promise<string | void>;
|
||||
// Run effects the CALLER owns and will perform itself — those runs are skipped
|
||||
// (not executed). e.g. a headless rebuild or a setup that restarts once at the
|
||||
// end passes ['restart']; applyProviderSkill passes ['build','test'].
|
||||
skipEffects?: string[];
|
||||
// Resolve which remote carries a `from-branch` registry branch. Defaults to a
|
||||
// generic resolver (env override → first remote that has the branch → origin);
|
||||
// setup injects one that reuses setup/lib/channels-remote.sh for exact parity.
|
||||
@@ -411,6 +415,11 @@ export async function applySkill(skillDir: string, root: string, opts: ApplyOpti
|
||||
res.applied.push(`operator: ${(d.body[0] ?? '').slice(0, 50)}`);
|
||||
continue;
|
||||
}
|
||||
// A run whose effect the caller owns (e.g. restart) is skipped here.
|
||||
if (d.kind === 'run' && typeof d.attrs.effect === 'string' && opts.skipEffects?.includes(d.attrs.effect)) {
|
||||
res.skipped.push(`run ${d.attrs.effect}: owned by the caller`);
|
||||
continue;
|
||||
}
|
||||
const st = selfStatus(d, root);
|
||||
if (st.status === 'agent') { bounce(d, 'no deterministic handler'); continue; }
|
||||
if (st.status === 'skip') { res.skipped.push(`${d.kind}: ${st.detail}`); continue; }
|
||||
|
||||
@@ -21,6 +21,7 @@ describe('skill-directives parser, on the converted add-slack', () => {
|
||||
'env-set', // credentials: write captured values to .env
|
||||
'env-sync', // credentials: sync to container
|
||||
'operator', // credentials: event-delivery walkthrough
|
||||
'run', // restart: load the adapter + creds, wait for the CLI socket
|
||||
'prompt', // wire: owner member id
|
||||
'prompt', // wire: target agent folder
|
||||
'run', // wire: validate token (auth.test)
|
||||
@@ -58,6 +59,7 @@ describe('skill-directives parser, on the converted add-slack', () => {
|
||||
expect(directives.filter((d) => d.kind === 'run').map((d) => d.attrs.effect)).toEqual([
|
||||
'build',
|
||||
'test',
|
||||
'restart', // load adapter + creds before wiring
|
||||
'fetch', // validate: auth.test
|
||||
'fetch', // resolve: conversations.open
|
||||
'wire', // ncl wiring
|
||||
|
||||
@@ -20,12 +20,14 @@
|
||||
// copy [from-branch:<b>] body: `PATH` (src==dst) or `SRC -> DST` overwrite
|
||||
// append to:<file> [at:<marker>] body: line(s) to add skip if present
|
||||
// dep [manager:pnpm] body: `pkg@<exact-semver>` line(s) reinstall no-op
|
||||
// run [effect:build|test|fetch|external|wire] [capture:<var>] re-runnable
|
||||
// run [effect:build|test|fetch|external|wire|restart] [capture:<var>] re-runnable
|
||||
// body: shell command(s). {{vars}} are substituted in. effect:wire runs
|
||||
// `ncl …` to wire collected input (no undo — the rows it creates are user
|
||||
// runtime data, not reversed on skill remove). capture:<var> binds the
|
||||
// command's stdout into {{var}} (twin of prompt) — e.g. resolve an id
|
||||
// from an API and feed it to a later directive.
|
||||
// runtime data, not reversed on skill remove). effect:restart restarts the
|
||||
// service so following `ncl` runs reach it; a caller that owns the restart
|
||||
// (rebuild, or a setup that restarts once) skips it via ApplyOptions.
|
||||
// skipEffects. capture:<var> binds the command's stdout into {{var}} (twin
|
||||
// of prompt) — e.g. resolve an id from an API and feed it to a later step.
|
||||
// prompt <var> [secret] body: the question → binds {{var}} skip if satisfied
|
||||
// operator body: instructions for the human operator output-only
|
||||
// The SKILL.md is addressed to the coding agent; `operator` delineates the
|
||||
|
||||
Executable
+25
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env bash
|
||||
# Restart the NanoClaw service, then wait (best-effort) for its `ncl` CLI socket
|
||||
# so a following wiring directive doesn't race the restart. Channel skills call
|
||||
# this as `nc:run effect:restart`. Best-effort throughout: a fresh setup may not
|
||||
# have the service installed yet, and the wiring's own `ncl` call is the real
|
||||
# signal if the socket never appears — so a wait timeout does not fail the step.
|
||||
set -u
|
||||
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
root="$(cd "$here/../.." && pwd)"
|
||||
# shellcheck source=/dev/null
|
||||
source "$here/install-slug.sh"
|
||||
|
||||
case "$(uname -s)" in
|
||||
Darwin) launchctl kickstart -k "gui/$(id -u)/$(launchd_label)" 2>/dev/null || true ;;
|
||||
Linux) systemctl --user restart "$(systemd_unit)" 2>/dev/null \
|
||||
|| sudo systemctl restart "$(systemd_unit)" 2>/dev/null || true ;;
|
||||
esac
|
||||
|
||||
# Wait up to ~30s for the CLI socket so `ncl` can connect on the next directive.
|
||||
for _ in $(seq 1 60); do
|
||||
[ -S "$root/data/ncl.sock" ] && exit 0
|
||||
sleep 0.5
|
||||
done
|
||||
echo "nanoclaw: ncl socket not up yet after restart — the wiring step may need a retry" >&2
|
||||
exit 0
|
||||
Reference in New Issue
Block a user