mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
feat(skill): spawn and drive DeepSeek Harness workers
The agent skill could spawn a worker in any mode, but it could only
DRIVE a claude one: every other CLI has neither a real end-of-turn
signal nor an answer to read, so the recipes route them through output
markers.
dsh has both halves now -- its harness reports idle/working/blocked to
Codeman, and the previous commit reads its transcript -- so it joins
claude as a mode the four verbs work on unchanged. `spawn_workers alpha
beta:deepseek` is a mixed fleet in one call, and `sendwait` / `last_text`
/ `delete_session` need no per-mode variant.
Preamble 1.20.0 (SKILL.md's §0 heredoc regenerated from it):
- `spawn_worker` grows a deepseek branch that gates on the harness
composer. ⚠️ Readiness there is NOT the stop signal: the harness
reports idle at BOOT ~300 ms before its composer paints (measured
2.26 s vs 2.56 s after spawn), so a send-and-wait fired straight after
quick-start resolves on the boot edge, reports a turn that never ran,
and strands the prompt in a pane not yet taking input. Waiting for the
composer also spends that edge, since signals are edge-triggered.
- `spawn_workers` takes `name[:mode]`, so a mixed fleet stays one
concurrent call. Case names still have to be unique -- the mode never
disambiguates two workers that would share a directory.
- `sendwait` asks for `wait:"stop,exit"` instead of the `wait:true`
default set. That set also carries `idle`, which for an external CLI is
inferred from output stabilization: on a dsh worker whose TUI repaints
rarely, the re-wait resolved in 0 ms with `signal:"idle"` on a turn
with three minutes left to run. It also makes a wrong mode loud -- the
modes that cannot deliver `stop` answer 400 before writing anything,
instead of resolving on a flap.
- The self-heal resend carries `delivered:true` forward. The resend is a
tagged duplicate, so the server truthfully reports `delivered:false`
about a write it skipped, and §1's cleanup then read a completed turn
as an undelivered one and kept a finished worker forever.
- dsh workers spawn with the permission posture the Run button sends,
because the harness default still asks and a worker parked on an
approval row cannot finish a fan-out. The multi-user clamp still
applies.
Docs: a worked dsh flow in recipes.md, readiness and the signal rules in
verbs.md, and the corrections this makes necessary -- `stop`/`blocked`
are no longer claude-only, and `last-response` is no longer permanently
empty for deepseek. The integration guide gains a section on reading a
session back and driving one as a worker; its web-UI section was also
stale (that server moved out of a shell session).
The static guard that keeps those lists from naming some external CLIs but
not others is extended rather than exempted: it now knows the three real
classes inside that family (no transcript, no hook signals, and the
positive twin -- the modes whose answers can be read), with the hook class
derived from `hooksAvailableForMode()` so the predicate and the prose
cannot drift apart. Any other partial list still fails, and a new backend
belongs to none of the classes until someone says so.
This commit is contained in:
@@ -26,11 +26,20 @@
|
||||
* external CLIs: those lists exist to describe what `isExternalCliMode()` gates
|
||||
* (no Claude transcript, no hooks, no Claude-format parsers), so naming some but
|
||||
* not all of them is the drift itself. Runs of one or two modes are exempt, since
|
||||
* a legitimate pair ("claude or shell") is not a class claim. ONE exception is
|
||||
* allowed and it is a real one: the "writes no transcript" lists drop `codex`,
|
||||
* which does write a rollout Codeman reads back (the pane carries a unique
|
||||
* originator precisely so `last-response` can find it), so external-minus-codex
|
||||
* is a meaningful class rather than an oversight.
|
||||
* a legitimate pair ("claude or shell") is not a class claim. The exceptions are
|
||||
* the REAL classes inside the external family, each one a capability some of those
|
||||
* CLIs have and the rest do not:
|
||||
*
|
||||
* - "writes no transcript" drops `codex` (a rollout Codeman reads back) and
|
||||
* `deepseek` (a JSONL session file Codeman reads back);
|
||||
* - "delivers no hook signals" drops `deepseek`, whose harness reports its own
|
||||
* lifecycle -- that one is derived from `hooksAvailableForMode()` rather than
|
||||
* restated, so the predicate and the prose cannot drift apart;
|
||||
* - the positive twin of the first: the modes whose answers CAN be read.
|
||||
*
|
||||
* Anything else partial is still the drift. A NEW backend belongs to none of these
|
||||
* classes until someone says so, so every one of them grows by a mode and every
|
||||
* stale list fails here -- which is the whole point.
|
||||
*
|
||||
* Port: N/A (pure static analysis).
|
||||
*/
|
||||
@@ -41,6 +50,7 @@ import { fileURLToPath } from 'node:url';
|
||||
import { join } from 'node:path';
|
||||
import { CreateSessionSchema, QuickStartSchema } from '../src/web/schemas.js';
|
||||
import { isExternalCliMode } from '../src/session.js';
|
||||
import { hooksAvailableForMode } from '../src/web/session-wait-registry.js';
|
||||
import type { SessionMode } from '../src/types/session.js';
|
||||
|
||||
const HERE = fileURLToPath(new URL('.', import.meta.url));
|
||||
@@ -63,6 +73,15 @@ function schemaModes(schema: typeof CreateSessionSchema | typeof QuickStartSchem
|
||||
const MODES = schemaModes(CreateSessionSchema);
|
||||
const EXTERNAL_MODES = MODES.filter(isExternalCliMode);
|
||||
|
||||
/**
|
||||
* External modes whose ANSWERS Codeman can read: codex from its rollout,
|
||||
* deepseek from `$DSH_HOME/sessions/**`. Stated here rather than derived because
|
||||
* `last-response` branches per mode into a per-CLI reader and there is no single
|
||||
* predicate to import; the runtime facts are `readCodexLastResponse` and
|
||||
* `readDeepSeekLastResponse` in session-routes.ts.
|
||||
*/
|
||||
const TRANSCRIPT_EXTERNAL_MODES = new Set<string>(['codex', 'deepseek']);
|
||||
|
||||
/**
|
||||
* Mode tokens appearing back to back, separated only by list punctuation — `a|b|c`,
|
||||
* `a`/`b`/`c`, "`a`, `b` and `c`". Newlines collapse to spaces first so a wrapped list
|
||||
@@ -109,9 +128,12 @@ describe('agent skill run-mode lists', () => {
|
||||
|
||||
it('never enumerates a partial set of external CLI modes', () => {
|
||||
const complete = new Set<string>(EXTERNAL_MODES);
|
||||
/** The documented exception: codex writes a rollout, so it is absent from the
|
||||
* "no transcript" lists on purpose. Every OTHER external mode must still be there. */
|
||||
const withoutCodex = new Set<string>(EXTERNAL_MODES.filter((m) => m !== 'codex'));
|
||||
// The real classes inside the family (see the fileoverview). Each is derived, so
|
||||
// an eighth backend joins none of them and every list naming the other seven fails.
|
||||
const noTranscript = new Set<string>(EXTERNAL_MODES.filter((m) => !TRANSCRIPT_EXTERNAL_MODES.has(m)));
|
||||
const withTranscript = new Set<string>(EXTERNAL_MODES.filter((m) => TRANSCRIPT_EXTERNAL_MODES.has(m)));
|
||||
const noHookSignals = new Set<string>(EXTERNAL_MODES.filter((m) => !hooksAvailableForMode(m)));
|
||||
const allowed = [complete, noTranscript, withTranscript, noHookSignals];
|
||||
const sameSet = (a: Set<string>, b: Set<string>) => a.size === b.size && [...a].every((v) => b.has(v));
|
||||
|
||||
const offenders: string[] = [];
|
||||
@@ -121,7 +143,7 @@ describe('agent skill run-mode lists', () => {
|
||||
if (listed.length < 3) continue;
|
||||
const externals = new Set<string>(listed.filter(isExternalCliMode));
|
||||
// Empty is fine (a claude/shell-only list); partial is the drift.
|
||||
if (externals.size === 0 || sameSet(externals, complete) || sameSet(externals, withoutCodex)) continue;
|
||||
if (externals.size === 0 || allowed.some((set) => sameSet(externals, set))) continue;
|
||||
const missing = EXTERNAL_MODES.filter((m) => !externals.has(m));
|
||||
offenders.push(`${file}: "${run.trim()}" is missing ${missing.join(', ')}`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user