Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions .claude/skills/dynamic-workflow-engineering/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
---
name: dynamic-workflow-engineering
description: Run a finite, runtime-issued Dynamic Workflow packet with deterministic preflight, per-run evidence, partial-result preservation, and honest capability tiers.
allowed-tools: Read, Glob, Grep, Bash, Write, Edit, Workflow, ToolSearch, mcp__dev-pomogator-specs__list_spec_docs, mcp__dev-pomogator-specs__read_spec_doc
---

# Dynamic Workflow Engineering

Use this skill when a task needs bounded multi-agent orchestration. Native Claude Code `Agent` is a separate subject and is never authorized by Workflow prose, labels, subtype, session, or environment.

## Canonical path

1. Build a finite packet that matches `tools/dynamic-workflow-engineering/contracts.json`.
2. Run the installed runtime preflight from `CLAUDE_PLUGIN_ROOT`:
`node "$CLAUDE_PLUGIN_ROOT/tools/dynamic-workflow-engineering/runtime.bundle.mjs" prepare <packet.json>`.
3. Continue only when the decision is `allow`, `state.json` reached `ROOT_VERIFIED`, and the runtime returned `preparedPacketPath`.
4. Invoke Workflow with `scriptPath: "$CLAUDE_PLUGIN_ROOT/tools/dynamic-workflow-engineering/workflow.mjs"` and `args: { preparedPacketPath }`. The script reads the runtime-created envelope itself; raw caller-created packet objects are rejected.
5. Finalize through the runtime journal/verification path; do not infer completion from agent prose.

## Packet rules

The packet declares finite scopes, population digest, work packages, ownership, dependencies, barriers, evidence/output contract, stop condition, blocked/dropped states, all ceilings, root/worktree/base SHA/dirty allowlist, required gates, and runtime-issued run/attempt/owner identity.

- Unknown-size work needs an explicit discovery bound.
- Deterministic inventory happens before model work.
- One logical call is distinct from physical attempts.
- At most one materially changed retry is allowed.
- Completed branch output survives sibling failure.
- `COMPLETE` requires every mandatory branch.
- Raw prompts, secrets, tokens, and tool payloads do not enter audit journals.

## Guarantee

Read the capability matrix. Publish exactly one tier:

- `ENFORCED` only with real direct and Workflow-nested native-Agent deny-before-spawn plus independent valid Workflow-native delivery.
- `STEERING_ONLY` when the bounded runtime works but native-Agent enforcement is unproven.
- `UNAVAILABLE` when the safe runtime path cannot operate.

Never install or describe a fake protected hook.
43 changes: 26 additions & 17 deletions .claude/skills/spec-generator-orchestrator/scripts/phase-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@
* @see .claude/agents/spec-phase-*.md
*/
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { runCapturedProcess } from '../../../../tools/dynamic-workflow-engineering/captured-process.ts';

export const PHASES = ['discovery', 'requirements', 'finalization', 'audit'] as const;
export type Phase = (typeof PHASES)[number];
Expand Down Expand Up @@ -58,24 +60,30 @@ export async function productionGate(slug: string): Promise<GateResult> {
* allowed-tools); we trust the verdict gate over its self-report, so the
* stdout is returned but not parsed.
*/
export function productionSpawn(phase: Phase, slug: string, gapList: string[]): Promise<string> {
return new Promise((resolve, reject) => {
import('node:child_process').then(({ spawn }) => {
const bin = process.env.CLAUDE_BIN ?? 'claude';
const gaps = gapList.length ? `\nOpen verdict gaps to fix:\n- ${gapList.join('\n- ')}` : '';
const prompt =
`You are the spec-phase-${phase} agent. Work ONLY through the ` +
`dev-pomogator-specs MCP tools (no file tools over .specs/). ` +
`Author the ${phase} phase of spec "${slug}".${gaps}`;
const child = spawn(bin, ['-p', '--agent', `spec-phase-${phase}`, prompt], {
stdio: ['ignore', 'pipe', 'pipe'],
});
const out: Buffer[] = [];
child.stdout.on('data', (c) => out.push(c));
child.on('error', reject);
child.on('exit', () => resolve(Buffer.concat(out).toString('utf8')));
});
export async function productionSpawn(phase: Phase, slug: string, gapList: string[]): Promise<string> {
const bin = process.env.CLAUDE_BIN ?? 'claude';
const boundedGaps = gapList.slice(0, 50).map((gap) => gap.slice(0, 1_000));
const gaps = boundedGaps.length ? `\nOpen verdict gaps to fix:\n- ${boundedGaps.join('\n- ')}` : '';
const prompt =
`You are the spec-phase-${phase} agent. Work ONLY through the ` +
`dev-pomogator-specs MCP tools (no file tools over .specs/). ` +
`Author the ${phase} phase of spec "${slug}".${gaps}`;
const evidenceDirectory = fs.mkdtempSync(path.join(os.tmpdir(), `dwe-spec-phase-${phase}-`));
const result = await runCapturedProcess({
executable: bin,
argv: ['-p', '--agent', `spec-phase-${phase}`, prompt],
cwd: process.cwd(),
evidenceDirectory,
timeoutMs: 15 * 60_000,
maxOutputBytes: 1_048_576,
});
const stdout = fs.readFileSync(result.stdoutRef, 'utf8');
const stderr = fs.readFileSync(result.stderrRef, 'utf8');
if (result.exitCode !== 0 || result.classification !== 'SUCCESS') {
const detail = stderr.trim() || stdout.trim() || result.classification;
throw new Error(`spec phase ${phase} child exited ${result.exitCode}: ${detail}`);
}
return stdout;
}

export interface PhaseRunEvent {
Expand Down Expand Up @@ -126,6 +134,7 @@ function defaultLogger(repoRoot: string): (e: PhaseRunEvent) => void {
*/
export async function runPhases(opts: PhaseRunOptions): Promise<PhaseRunResult> {
const maxRetries = opts.maxRetries ?? 2;
if (!Number.isSafeInteger(maxRetries) || maxRetries < 0 || maxRetries > 2) throw new Error('maxRetries must be an integer from 0 through 2');
const spawn = opts.spawn ?? productionSpawn; // real headless agent unless injected
const gate = opts.gate ?? productionGate; // real verdict unless injected
const emit = opts.onEvent ?? defaultLogger(opts.repoRoot ?? process.cwd());
Expand Down
Loading
Loading