11 Commits

Author SHA1 Message Date
Affaan Mustafa
d994e0503b test: fix cross-platform orchestration regressions 2026-03-12 15:46:50 -07:00
Affaan Mustafa
2d43541f0e fix: preserve orchestration launcher compatibility 2026-03-12 15:40:25 -07:00
Affaan Mustafa
c5b8a0783e fix: resolve lint regression in plan parsing 2026-03-12 15:35:12 -07:00
Affaan Mustafa
af318b8f04 fix: address remaining orchestration review comments 2026-03-12 15:34:05 -07:00
Affaan Mustafa
0d96876505 chore: resolve audit findings in lint tooling 2026-03-12 15:11:57 -07:00
Affaan Mustafa
52daf17cb5 fix: harden orchestration status and skill docs 2026-03-12 15:07:57 -07:00
Affaan Mustafa
ddab6f1190 fix: compare hook roots by file identity 2026-03-12 14:55:29 -07:00
Affaan Mustafa
9359e46951 fix: resolve exa skill markdown lint 2026-03-12 14:49:05 -07:00
Affaan Mustafa
ad4ef58a8e fix: resolve orchestration lint errors 2026-03-12 14:49:05 -07:00
Affaan Mustafa
4d4ba25d11 feat: add orchestration workflows and harness skills 2026-03-12 14:49:05 -07:00
Affaan Mustafa
d3f4fd5061 fix: restore mainline CI on Windows and markdown lint (#415)
* fix: restore ci compatibility on windows

* fix: normalize hook path assertions on windows

* fix: relax repo root assertion on windows

* fix: keep hook root assertion strict on windows
2026-03-12 14:48:21 -07:00
24 changed files with 572 additions and 123 deletions

View File

@@ -230,6 +230,8 @@ print(f"Cache creation: {message.usage.cache_creation_input_tokens}")
Process large volumes asynchronously at 50% cost reduction:
```python
import time
batch = client.messages.batches.create(
requests=[
{
@@ -306,6 +308,8 @@ while True:
## Error Handling
```python
import time
from anthropic import APIError, RateLimitError, APIConnectionError
try:

View File

@@ -8,14 +8,16 @@ origin: ECC
Distribute content across multiple social platforms with platform-native adaptation.
## When to Activate
## When to Use
- User wants to post content to multiple platforms
- Publishing announcements, launches, or updates across social media
- Repurposing a post from one platform to others
- User says "crosspost", "post everywhere", "share on all platforms", or "distribute this"
## Core Rules
## How It Works
### Core Rules
1. **Never post identical content cross-platform.** Each platform gets a native adaptation.
2. **Primary platform first.** Post to the main platform, then adapt for others.
@@ -23,7 +25,7 @@ Distribute content across multiple social platforms with platform-native adaptat
4. **One idea per post.** If the source content has multiple ideas, split across posts.
5. **Attribution matters.** If crossposting someone else's content, credit the source.
## Platform Specifications
### Platform Specifications
| Platform | Max Length | Link Handling | Hashtags | Media |
|----------|-----------|---------------|----------|-------|
@@ -32,7 +34,7 @@ Distribute content across multiple social platforms with platform-native adaptat
| Threads | 500 chars | Separate link attachment | None typical | Images, video |
| Bluesky | 300 chars | Via facets (rich text) | None (use feeds) | Images |
## Workflow
### Workflow
### Step 1: Create Source Content
@@ -87,7 +89,7 @@ Post adapted versions to remaining platforms:
- Stagger timing (not all at once — 30-60 min gaps)
- Include cross-platform references where appropriate ("longer thread on X" etc.)
## Content Adaptation Examples
## Examples
### Source: Product Launch
@@ -148,6 +150,7 @@ A pattern I've been using that's made a real difference:
If using a crossposting service (e.g., Postbridge, Buffer, or a custom API), the pattern looks like:
```python
import os
import requests
resp = requests.post(
@@ -160,8 +163,10 @@ resp = requests.post(
"linkedin": {"text": linkedin_version},
"threads": {"text": threads_version}
}
}
},
timeout=30
)
resp.raise_for_status()
```
### Manual Posting

View File

@@ -24,7 +24,11 @@ Exa MCP server must be configured. Add to `~/.claude.json`:
```json
"exa-web-search": {
"command": "npx",
"args": ["-y", "exa-mcp-server"],
"args": [
"-y",
"exa-mcp-server",
"tools=web_search_exa,web_search_advanced_exa,get_code_context_exa,crawling_exa,company_research_exa,people_search_exa,deep_researcher_start,deep_researcher_check"
],
"env": { "EXA_API_KEY": "YOUR_EXA_API_KEY_HERE" }
}
```

View File

@@ -92,6 +92,7 @@ def post_thread(oauth, tweets: list[str]) -> list[str]:
if reply_to:
payload["reply"] = {"in_reply_to_tweet_id": reply_to}
resp = oauth.post("https://api.x.com/2/tweets", json=payload)
resp.raise_for_status()
tweet_id = resp.json()["data"]["id"]
ids.append(tweet_id)
reply_to = tweet_id
@@ -167,6 +168,8 @@ resp = oauth.post(
Always check `x-rate-limit-remaining` and `x-rate-limit-reset` headers.
```python
import time
remaining = int(resp.headers.get("x-rate-limit-remaining", 0))
if remaining < 5:
reset = int(resp.headers.get("x-rate-limit-reset", 0))

109
package-lock.json generated
View File

@@ -18,7 +18,7 @@
"c8": "^10.1.2",
"eslint": "^9.39.2",
"globals": "^17.1.0",
"markdownlint-cli": "^0.47.0"
"markdownlint-cli": "^0.48.0"
},
"engines": {
"node": ">=18"
@@ -267,29 +267,6 @@
"url": "https://github.com/sponsors/nzakas"
}
},
"node_modules/@isaacs/balanced-match": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
"integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": "20 || >=22"
}
},
"node_modules/@isaacs/brace-expansion": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz",
"integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@isaacs/balanced-match": "^4.0.1"
},
"engines": {
"node": "20 || >=22"
}
},
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
@@ -714,11 +691,10 @@
"license": "MIT"
},
"node_modules/commander": {
"version": "14.0.2",
"resolved": "https://registry.npmjs.org/commander/-/commander-14.0.2.tgz",
"integrity": "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==",
"version": "14.0.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz",
"integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=20"
}
@@ -844,7 +820,6 @@
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.12"
},
@@ -1588,7 +1563,6 @@
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz",
"integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"uc.micro": "^2.0.0"
}
@@ -1640,11 +1614,10 @@
}
},
"node_modules/markdown-it": {
"version": "14.1.0",
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz",
"integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==",
"version": "14.1.1",
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz",
"integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==",
"dev": true,
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1",
"entities": "^4.4.0",
@@ -1682,23 +1655,22 @@
}
},
"node_modules/markdownlint-cli": {
"version": "0.47.0",
"resolved": "https://registry.npmjs.org/markdownlint-cli/-/markdownlint-cli-0.47.0.tgz",
"integrity": "sha512-HOcxeKFAdDoldvoYDofd85vI8LgNWy8vmYpCwnlLV46PJcodmGzD7COSSBlhHwsfT4o9KrAStGodImVBus31Bg==",
"version": "0.48.0",
"resolved": "https://registry.npmjs.org/markdownlint-cli/-/markdownlint-cli-0.48.0.tgz",
"integrity": "sha512-NkZQNu2E0Q5qLEEHwWj674eYISTLD4jMHkBzDobujXd1kv+yCxi8jOaD/rZoQNW1FBBMMGQpuW5So8B51N/e0A==",
"dev": true,
"license": "MIT",
"dependencies": {
"commander": "~14.0.2",
"commander": "~14.0.3",
"deep-extend": "~0.6.0",
"ignore": "~7.0.5",
"js-yaml": "~4.1.1",
"jsonc-parser": "~3.3.1",
"jsonpointer": "~5.0.1",
"markdown-it": "~14.1.0",
"markdown-it": "~14.1.1",
"markdownlint": "~0.40.0",
"minimatch": "~10.1.1",
"minimatch": "~10.2.4",
"run-con": "~1.3.2",
"smol-toml": "~1.5.2",
"smol-toml": "~1.6.0",
"tinyglobby": "~0.2.15"
},
"bin": {
@@ -1708,6 +1680,27 @@
"node": ">=20"
}
},
"node_modules/markdownlint-cli/node_modules/balanced-match": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
"dev": true,
"engines": {
"node": "18 || 20 || >=22"
}
},
"node_modules/markdownlint-cli/node_modules/brace-expansion": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz",
"integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==",
"dev": true,
"dependencies": {
"balanced-match": "^4.0.2"
},
"engines": {
"node": "18 || 20 || >=22"
}
},
"node_modules/markdownlint-cli/node_modules/ignore": {
"version": "7.0.5",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
@@ -1719,16 +1712,15 @@
}
},
"node_modules/markdownlint-cli/node_modules/minimatch": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz",
"integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==",
"version": "10.2.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz",
"integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
"@isaacs/brace-expansion": "^5.0.0"
"brace-expansion": "^5.0.2"
},
"engines": {
"node": "20 || >=22"
"node": "18 || 20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
@@ -1738,8 +1730,7 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz",
"integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==",
"dev": true,
"license": "MIT"
"dev": true
},
"node_modules/micromark": {
"version": "4.0.2",
@@ -2278,11 +2269,10 @@
"license": "MIT"
},
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
},
@@ -2489,7 +2479,6 @@
"resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz",
"integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
@@ -2590,11 +2579,10 @@
}
},
"node_modules/smol-toml": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.5.2.tgz",
"integrity": "sha512-QlaZEqcAH3/RtNyet1IPIYPsEWAaYyXXv1Krsi+1L/QHppjX4Ifm8MQsBISz9vE8cHicIq3clogsheili5vhaQ==",
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.6.0.tgz",
"integrity": "sha512-4zemZi0HvTnYwLfrpk/CF9LOd9Lt87kAt50GnqhMpyF9U3poDAP2+iukq2bZsO/ufegbYehBkqINbsWxj4l4cw==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">= 18"
},
@@ -2812,8 +2800,7 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
"integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==",
"dev": true,
"license": "MIT"
"dev": true
},
"node_modules/uri-js": {
"version": "4.4.1",

View File

@@ -98,7 +98,7 @@
"c8": "^10.1.2",
"eslint": "^9.39.2",
"globals": "^17.1.0",
"markdownlint-cli": "^0.47.0"
"markdownlint-cli": "^0.48.0"
},
"engines": {
"node": ">=18"

View File

@@ -17,6 +17,16 @@ function stripCodeTicks(value) {
return trimmed;
}
function normalizeSessionName(value, fallback = 'session') {
const normalized = String(value || '')
.trim()
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '');
return normalized || fallback;
}
function parseSection(content, heading) {
if (typeof content !== 'string' || content.length === 0) {
return '';
@@ -96,11 +106,31 @@ function parseWorkerTask(content) {
};
}
function parseFirstSection(content, headings) {
for (const heading of headings) {
const section = parseSection(content, heading);
if (section) {
return section;
}
}
return '';
}
function parseWorkerHandoff(content) {
return {
summary: parseBullets(parseSection(content, 'Summary')),
validation: parseBullets(parseSection(content, 'Validation')),
remainingRisks: parseBullets(parseSection(content, 'Remaining Risks'))
summary: parseBullets(parseFirstSection(content, ['Summary'])),
validation: parseBullets(parseFirstSection(content, [
'Validation',
'Tests / Verification',
'Tests',
'Verification'
])),
remainingRisks: parseBullets(parseFirstSection(content, [
'Remaining Risks',
'Follow-ups',
'Follow Ups'
]))
};
}
@@ -240,28 +270,66 @@ function buildSessionSnapshot({ sessionName, coordinationDir, panes }) {
};
}
function readPlanConfig(absoluteTarget) {
let config;
try {
config = JSON.parse(fs.readFileSync(absoluteTarget, 'utf8'));
} catch (_error) {
throw new Error(`Invalid orchestration plan JSON: ${absoluteTarget}`);
}
if (!config || Array.isArray(config) || typeof config !== 'object') {
throw new Error(`Invalid orchestration plan: expected a JSON object (${absoluteTarget})`);
}
return config;
}
function readPlanString(config, key, absoluteTarget) {
const value = config[key];
if (value === undefined) {
return undefined;
}
if (typeof value !== 'string') {
throw new Error(`Invalid orchestration plan: ${key} must be a string when provided (${absoluteTarget})`);
}
const normalized = value.trim();
return normalized.length > 0 ? normalized : undefined;
}
function resolveSnapshotTarget(targetPath, cwd = process.cwd()) {
const absoluteTarget = path.resolve(cwd, targetPath);
if (fs.existsSync(absoluteTarget) && fs.statSync(absoluteTarget).isFile()) {
const config = JSON.parse(fs.readFileSync(absoluteTarget, 'utf8'));
const repoRoot = path.resolve(config.repoRoot || cwd);
const config = readPlanConfig(absoluteTarget);
const repoRoot = path.resolve(readPlanString(config, 'repoRoot', absoluteTarget) || cwd);
const sessionName = normalizeSessionName(
readPlanString(config, 'sessionName', absoluteTarget) || path.basename(repoRoot),
'session'
);
const coordinationRoot = path.resolve(
config.coordinationRoot || path.join(repoRoot, '.orchestration')
readPlanString(config, 'coordinationRoot', absoluteTarget) || path.join(repoRoot, '.orchestration')
);
return {
sessionName: config.sessionName,
coordinationDir: path.join(coordinationRoot, config.sessionName),
sessionName,
coordinationDir: path.join(coordinationRoot, sessionName),
repoRoot,
targetType: 'plan'
};
}
const repoRoot = path.resolve(cwd);
const sessionName = normalizeSessionName(targetPath, path.basename(repoRoot));
return {
sessionName: targetPath,
coordinationDir: path.join(cwd, '.claude', 'orchestration', targetPath),
repoRoot: cwd,
sessionName,
coordinationDir: path.join(repoRoot, '.orchestration', sessionName),
repoRoot,
targetType: 'session'
};
}

View File

@@ -34,6 +34,18 @@ function formatCommand(program, args) {
return [program, ...args.map(shellQuote)].join(' ');
}
function buildTemplateVariables(values) {
return Object.entries(values).reduce((accumulator, [key, value]) => {
const stringValue = String(value);
const quotedValue = shellQuote(stringValue);
accumulator[key] = stringValue;
accumulator[`${key}_raw`] = stringValue;
accumulator[`${key}_sh`] = quotedValue;
return accumulator;
}, {});
}
function normalizeSeedPaths(seedPaths, repoRoot) {
const resolvedRepoRoot = path.resolve(repoRoot);
const entries = Array.isArray(seedPaths) ? seedPaths : [];
@@ -56,6 +68,12 @@ function normalizeSeedPaths(seedPaths, repoRoot) {
}
const normalizedPath = relativePath.split(path.sep).join('/');
if (!normalizedPath || normalizedPath === '.') {
throw new Error('seedPaths entries must not target the repo root');
}
if (normalizedPath === '.git' || normalizedPath.startsWith('.git/')) {
throw new Error(`seedPaths entries must not target git metadata: ${entry}`);
}
if (seen.has(normalizedPath)) {
continue;
}
@@ -120,6 +138,13 @@ function buildWorkerArtifacts(workerPlan) {
'## Completion',
'Do not spawn subagents or external agents for this task.',
'Report results in your final response.',
'Respond with these exact sections so orchestration parsing can succeed:',
'## Summary',
'- ...',
'## Validation',
'- ...',
'## Remaining Risks',
'- ...',
`The worker launcher captures your response in \`${workerPlan.handoffFilePath}\` automatically.`,
`The worker launcher updates \`${workerPlan.statusFilePath}\` automatically.`
].join('\n')
@@ -132,13 +157,10 @@ function buildWorkerArtifacts(workerPlan) {
'## Summary',
'- Pending',
'',
'## Files Changed',
'## Validation',
'- Pending',
'',
'## Tests / Verification',
'- Pending',
'',
'## Follow-ups',
'## Remaining Risks',
'- Pending'
].join('\n')
},
@@ -174,6 +196,7 @@ function buildOrchestrationPlan(config = {}) {
throw new Error('buildOrchestrationPlan requires at least one worker');
}
const seenWorkerSlugs = new Map();
const workerPlans = workers.map((worker, index) => {
if (!worker || typeof worker.task !== 'string' || worker.task.trim().length === 0) {
throw new Error(`Worker ${index + 1} is missing a task`);
@@ -181,6 +204,13 @@ function buildOrchestrationPlan(config = {}) {
const workerName = worker.name || `worker-${index + 1}`;
const workerSlug = slugify(workerName, `worker-${index + 1}`);
if (seenWorkerSlugs.has(workerSlug)) {
const firstWorkerName = seenWorkerSlugs.get(workerSlug);
throw new Error(
`Worker names must map to unique slugs: ${workerSlug} (${firstWorkerName}, ${workerName})`
);
}
seenWorkerSlugs.set(workerSlug, workerName);
const branchName = `orchestrator-${sessionName}-${workerSlug}`;
const worktreePath = path.join(worktreeRoot, `${repoName}-${sessionName}-${workerSlug}`);
const workerCoordinationDir = path.join(coordinationDir, workerSlug);
@@ -190,7 +220,7 @@ function buildOrchestrationPlan(config = {}) {
const launcherCommand = worker.launcherCommand || defaultLauncher;
const workerSeedPaths = normalizeSeedPaths(worker.seedPaths, repoRoot);
const seedPaths = normalizeSeedPaths([...globalSeedPaths, ...workerSeedPaths], repoRoot);
const templateVariables = {
const templateVariables = buildTemplateVariables({
branch_name: branchName,
handoff_file: handoffFilePath,
repo_root: repoRoot,
@@ -200,7 +230,7 @@ function buildOrchestrationPlan(config = {}) {
worker_name: workerName,
worker_slug: workerSlug,
worktree_path: worktreePath
};
});
if (!launcherCommand) {
throw new Error(`Worker ${workerName} is missing a launcherCommand`);

View File

@@ -51,11 +51,11 @@ Rules:
- Report progress and final results in stdout only.
- Do not write handoff or status files yourself; the launcher manages those artifacts.
- If you change code or docs, keep the scope narrow and defensible.
- In your final response, include exactly these sections:
- In your final response, include these exact sections:
1. Summary
2. Files Changed
3. Validation
4. Remaining Risks
2. Validation
3. Remaining Risks
- You may include Files Changed if useful, but keep the three sections above exact.
Task file: $task_file

View File

@@ -17,8 +17,11 @@ function usage() {
' node scripts/orchestrate-worktrees.js <plan.json> [--write-only]',
'',
'Placeholders supported in launcherCommand:',
' {worker_name} {worker_slug} {session_name} {repo_root}',
' {worktree_path} {branch_name} {task_file} {handoff_file} {status_file}',
' Raw defaults: {worker_name} {worker_slug} {session_name} {repo_root}',
' Raw defaults: {worktree_path} {branch_name} {task_file} {handoff_file} {status_file}',
' Shell-safe aliases: {worker_name_sh} {worker_slug_sh} {session_name_sh} {repo_root_sh}',
' Shell-safe aliases: {worktree_path_sh} {branch_name_sh} {task_file_sh} {handoff_file_sh} {status_file_sh}',
' Explicit raw aliases also exist with the _raw suffix.',
'',
'Without flags the script prints a dry-run plan only.'
].join('\n'));

View File

@@ -20,9 +20,32 @@ function usage() {
function parseArgs(argv) {
const args = argv.slice(2);
const target = args.find(arg => !arg.startsWith('--'));
const writeIndex = args.indexOf('--write');
const writePath = writeIndex >= 0 ? args[writeIndex + 1] : null;
let target = null;
let writePath = null;
for (let index = 0; index < args.length; index += 1) {
const arg = args[index];
if (arg === '--write') {
const candidate = args[index + 1];
if (!candidate || candidate.startsWith('--')) {
throw new Error('--write requires an output path');
}
writePath = candidate;
index += 1;
continue;
}
if (arg.startsWith('--')) {
throw new Error(`Unknown flag: ${arg}`);
}
if (target) {
throw new Error('Expected a single session name or plan path');
}
target = arg;
}
return { target, writePath };
}
@@ -56,4 +79,4 @@ if (require.main === module) {
}
}
module.exports = { main };
module.exports = { main, parseArgs };

View File

@@ -82,7 +82,7 @@ If the user chooses niche or core + niche, continue to category selection below
### 2b: Choose Skill Categories
There are 35 skills organized into 7 categories. Use `AskUserQuestion` with `multiSelect: true`:
There are 41 skills organized into 8 categories. Use `AskUserQuestion` with `multiSelect: true`:
```
Question: "Which skill categories do you want to install?"
@@ -90,6 +90,7 @@ Options:
- "Framework & Language" — "Django, Spring Boot, Go, Python, Java, Frontend, Backend patterns"
- "Database" — "PostgreSQL, ClickHouse, JPA/Hibernate patterns"
- "Workflow & Quality" — "TDD, verification, learning, security review, compaction"
- "Business & Content" — "Article writing, content engine, market research, investor materials, outreach"
- "Research & APIs" — "Deep research, Exa search, Claude API patterns"
- "Social & Content Distribution" — "X/Twitter API, crossposting alongside content-engine"
- "Media Generation" — "fal.ai image/video/audio alongside VideoDB"

View File

@@ -8,14 +8,16 @@ origin: ECC
Distribute content across multiple social platforms with platform-native adaptation.
## When to Activate
## When to Use
- User wants to post content to multiple platforms
- Publishing announcements, launches, or updates across social media
- Repurposing a post from one platform to others
- User says "crosspost", "post everywhere", "share on all platforms", or "distribute this"
## Core Rules
## How It Works
### Core Rules
1. **Never post identical content cross-platform.** Each platform gets a native adaptation.
2. **Primary platform first.** Post to the main platform, then adapt for others.
@@ -23,7 +25,7 @@ Distribute content across multiple social platforms with platform-native adaptat
4. **One idea per post.** If the source content has multiple ideas, split across posts.
5. **Attribution matters.** If crossposting someone else's content, credit the source.
## Platform Specifications
### Platform Specifications
| Platform | Max Length | Link Handling | Hashtags | Media |
|----------|-----------|---------------|----------|-------|
@@ -32,7 +34,7 @@ Distribute content across multiple social platforms with platform-native adaptat
| Threads | 500 chars | Separate link attachment | None typical | Images, video |
| Bluesky | 300 chars | Via facets (rich text) | None (use feeds) | Images |
## Workflow
### Workflow
### Step 1: Create Source Content
@@ -87,7 +89,7 @@ Post adapted versions to remaining platforms:
- Stagger timing (not all at once — 30-60 min gaps)
- Include cross-platform references where appropriate ("longer thread on X" etc.)
## Content Adaptation Examples
## Examples
### Source: Product Launch
@@ -161,8 +163,10 @@ resp = requests.post(
"linkedin": {"text": linkedin_version},
"threads": {"text": threads_version}
}
}
},
timeout=30
)
resp.raise_for_status()
```
### Manual Posting

View File

@@ -150,7 +150,7 @@ Example `plan.json`:
{
"sessionName": "skill-audit",
"baseRef": "HEAD",
"launcherCommand": "codex exec --cwd {worktree_path} --task-file {task_file}",
"launcherCommand": "codex exec --cwd {worktree_path_sh} --task-file {task_file_sh}",
"workers": [
{ "name": "docs-a", "task": "Fix skills 1-4 and write handoff notes." },
{ "name": "docs-b", "task": "Fix skills 5-8 and write handoff notes." }
@@ -176,7 +176,7 @@ Use `seedPaths` when workers need access to dirty or untracked local files that
"scripts/lib/tmux-worktree-orchestrator.js",
".claude/plan/workflow-e2e-test.json"
],
"launcherCommand": "bash {repo_root}/scripts/orchestrate-codex-worker.sh {task_file} {handoff_file} {status_file}",
"launcherCommand": "bash {repo_root_sh}/scripts/orchestrate-codex-worker.sh {task_file_sh} {handoff_file_sh} {status_file_sh}",
"workers": [
{ "name": "seed-check", "task": "Verify seeded files are present before starting work." }
]

View File

@@ -27,7 +27,7 @@ Exa MCP server must be configured. Add to `~/.claude.json`:
"args": [
"-y",
"exa-mcp-server",
"tools=web_search_exa,get_code_context_exa,crawling_exa,company_research_exa,linkedin_search_exa,deep_researcher_start,deep_researcher_check"
"tools=web_search_exa,web_search_advanced_exa,get_code_context_exa,crawling_exa,company_research_exa,people_search_exa,deep_researcher_start,deep_researcher_check"
],
"env": { "EXA_API_KEY": "YOUR_EXA_API_KEY_HERE" }
}
@@ -103,11 +103,11 @@ company_research_exa(companyName: "Anthropic", numResults: 5)
| `companyName` | string | required | Company name |
| `numResults` | number | 5 | Number of results |
### linkedin_search_exa
Find professional profiles and company-adjacent people research.
### people_search_exa
Find professional profiles and bios.
```
linkedin_search_exa(query: "AI safety researchers at Anthropic", numResults: 5)
people_search_exa(query: "AI safety researchers at Anthropic", numResults: 5)
```
### crawling_exa

View File

@@ -253,7 +253,7 @@ estimate_cost(
estimate_type: "unit_price",
endpoints: {
"fal-ai/nano-banana-pro": {
"num_images": 1
"unit_quantity": 1
}
}
)

View File

@@ -108,7 +108,7 @@ The user must set `VIDEO_DB_API_KEY` using **either** method:
- **Export in terminal** (before starting Claude): `export VIDEO_DB_API_KEY=your-key`
- **Project `.env` file**: Save `VIDEO_DB_API_KEY=your-key` in the project's `.env` file
Get a free API key at https://console.videodb.io (50 free uploads, no credit card).
Get a free API key at [console.videodb.io](https://console.videodb.io) (50 free uploads, no credit card).
**Do NOT** read, write, or handle the API key yourself. Always let the user set it.
@@ -354,7 +354,6 @@ Reference documentation is in the `reference/` directory adjacent to this SKILL.
- [reference/capture-reference.md](reference/capture-reference.md) - Capture SDK and WebSocket events
- [reference/use-cases.md](reference/use-cases.md) - Common video processing patterns and examples
**Do not use ffmpeg, moviepy, or local encoding tools** when VideoDB supports the operation. The following are all handled server-side by VideoDB — trimming, combining clips, overlaying audio or music, adding subtitles, text/image overlays, transcoding, resolution changes, aspect-ratio conversion, resizing for platform requirements, transcription, and media generation. Only fall back to local tools for operations listed under Limitations in reference/editor.md (transitions, speed changes, crop/zoom, colour grading, volume mixing).
### When to use what

View File

@@ -380,7 +380,7 @@ results = video.search(
```
> **Note:** `filter` is an explicit named parameter in `video.search()`. `scene_index_id` is passed through `**kwargs` to the API.
>
> **Important:** `video.search()` raises `InvalidRequestError` with message `"No results found"` when there are no matches. Always wrap search calls in try/except. For scene search, use `score_threshold=0.3` or higher to filter low-relevance noise.
For scene search, use `search_type=SearchType.semantic` with `index_type=IndexType.scene`. Pass `scene_index_id` when targeting a specific scene index. See [search.md](search.md) for details.

View File

@@ -107,7 +107,7 @@ Use [scripts/ws_listener.py](../scripts/ws_listener.py) to connect and dump even
}
```
> For latest details, see https://docs.videodb.io/pages/ingest/capture-sdks/realtime-context.md
> For latest details, see [the realtime context docs](https://docs.videodb.io/pages/ingest/capture-sdks/realtime-context.md).
---

View File

@@ -92,6 +92,7 @@ def post_thread(oauth, tweets: list[str]) -> list[str]:
if reply_to:
payload["reply"] = {"in_reply_to_tweet_id": reply_to}
resp = oauth.post("https://api.x.com/2/tweets", json=payload)
resp.raise_for_status()
tweet_id = resp.json()["data"]["id"]
ids.append(tweet_id)
reply_to = tweet_id

View File

@@ -98,6 +98,44 @@ function cleanupTestDir(testDir) {
fs.rmSync(testDir, { recursive: true, force: true });
}
function normalizeComparablePath(targetPath) {
if (!targetPath) return '';
let normalizedPath = String(targetPath).trim().replace(/\\/g, '/');
if (/^\/[a-zA-Z]\//.test(normalizedPath)) {
normalizedPath = `${normalizedPath[1]}:/${normalizedPath.slice(3)}`;
}
if (/^[a-zA-Z]:\//.test(normalizedPath)) {
normalizedPath = `${normalizedPath[0].toUpperCase()}:${normalizedPath.slice(2)}`;
}
try {
normalizedPath = fs.realpathSync(normalizedPath);
} catch {
// Fall through to string normalization when the path cannot be resolved directly.
}
return path.normalize(normalizedPath).replace(/\\/g, '/').replace(/^([a-z]):/, (_, drive) => `${drive.toUpperCase()}:`);
}
function pathsReferToSameLocation(leftPath, rightPath) {
const normalizedLeftPath = normalizeComparablePath(leftPath);
const normalizedRightPath = normalizeComparablePath(rightPath);
if (!normalizedLeftPath || !normalizedRightPath) return false;
if (normalizedLeftPath === normalizedRightPath) return true;
try {
const leftStats = fs.statSync(normalizedLeftPath);
const rightStats = fs.statSync(normalizedRightPath);
return leftStats.dev === rightStats.dev && leftStats.ino === rightStats.ino;
} catch {
return false;
}
}
function createCommandShim(binDir, baseName, logFile) {
fs.mkdirSync(binDir, { recursive: true });
@@ -2229,9 +2267,9 @@ async function runTests() {
assert.strictEqual(code, 0, `detect-project should source cleanly, stderr: ${stderr}`);
const [projectId, projectDir] = stdout.trim().split(/\r?\n/);
const [projectId] = stdout.trim().split(/\r?\n/);
const registryPath = path.join(homeDir, '.claude', 'homunculus', 'projects.json');
const projectMetadataPath = path.join(projectDir, 'project.json');
const projectMetadataPath = path.join(homeDir, '.claude', 'homunculus', 'projects', projectId, 'project.json');
assert.ok(projectId, 'detect-project should emit a project id');
assert.ok(fs.existsSync(registryPath), 'projects.json should be created');
@@ -2243,7 +2281,13 @@ async function runTests() {
assert.ok(registry[projectId], 'registry should contain the detected project');
assert.strictEqual(metadata.id, projectId, 'project.json should include the detected id');
assert.strictEqual(metadata.name, path.basename(repoDir), 'project.json should include the repo name');
assert.strictEqual(fs.realpathSync(metadata.root), fs.realpathSync(repoDir), 'project.json should include the repo root');
const normalizedMetadataRoot = normalizeComparablePath(metadata.root);
const normalizedRepoDir = normalizeComparablePath(repoDir);
assert.ok(normalizedMetadataRoot, 'project.json should include a non-empty repo root');
assert.ok(
pathsReferToSameLocation(normalizedMetadataRoot, normalizedRepoDir),
`project.json should include the repo root (expected ${normalizedRepoDir}, got ${normalizedMetadataRoot})`,
);
assert.strictEqual(metadata.remote, 'https://github.com/example/ecc-test.git', 'project.json should include the sanitized remote');
assert.ok(metadata.created_at, 'project.json should include created_at');
assert.ok(metadata.last_seen, 'project.json should include last_seen');
@@ -4526,10 +4570,20 @@ async function runTests() {
const files = fs.readdirSync(sessionsDir).filter(f => f.endsWith('.tmp'));
assert.ok(files.length > 0, 'Should create session file');
const content = fs.readFileSync(path.join(sessionsDir, files[0]), 'utf8');
const summaryMatch = content.match(
/<!-- ECC:SUMMARY:START -->([\s\S]*?)<!-- ECC:SUMMARY:END -->/
);
// The real string message should appear
assert.ok(content.includes('Real user message'), 'Should include the string content user message');
// Numeric/boolean/object content should NOT appear as text
assert.ok(!content.includes('42'), 'Numeric content should be skipped (else branch → empty string → filtered)');
assert.ok(summaryMatch, 'Should include a generated summary block');
const summaryBlock = summaryMatch[1];
// Numeric/boolean/object content should NOT appear as task bullets
assert.ok(
!summaryBlock.includes('\n- 42\n'),
'Numeric content should be skipped (else branch → empty string → filtered)'
);
assert.ok(!summaryBlock.includes('\n- true\n'), 'Boolean content should be skipped');
assert.ok(!summaryBlock.includes('[object Object]'), 'Object content should be skipped');
} finally {
fs.rmSync(isoHome, { recursive: true, force: true });
}

View File

@@ -109,6 +109,25 @@ test('parseWorkerHandoff also supports bold section headers', () => {
assert.deepStrictEqual(handoff.remainingRisks, ['No runtime screenshot']);
});
test('parseWorkerHandoff accepts legacy verification and follow-up headings', () => {
const handoff = parseWorkerHandoff([
'# Handoff',
'',
'## Summary',
'- Worker completed successfully',
'',
'## Tests / Verification',
'- Ran tests',
'',
'## Follow-ups',
'- Re-run screenshots after deploy'
].join('\n'));
assert.deepStrictEqual(handoff.summary, ['Worker completed successfully']);
assert.deepStrictEqual(handoff.validation, ['Ran tests']);
assert.deepStrictEqual(handoff.remainingRisks, ['Re-run screenshots after deploy']);
});
test('loadWorkerSnapshots reads coordination worker directories', () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-orch-session-'));
const coordinationDir = path.join(tempRoot, 'coordination');
@@ -204,7 +223,92 @@ test('resolveSnapshotTarget handles plan files and direct session names', () =>
const fromSession = resolveSnapshotTarget('workflow-visual-proof', repoRoot);
assert.strictEqual(fromSession.targetType, 'session');
assert.ok(fromSession.coordinationDir.endsWith(path.join('.claude', 'orchestration', 'workflow-visual-proof')));
assert.ok(fromSession.coordinationDir.endsWith(path.join('.orchestration', 'workflow-visual-proof')));
} finally {
fs.rmSync(tempRoot, { recursive: true, force: true });
}
});
test('resolveSnapshotTarget normalizes plan session names and defaults to the repo name', () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-orch-target-'));
const repoRoot = path.join(tempRoot, 'My Repo');
fs.mkdirSync(repoRoot, { recursive: true });
const namedPlanPath = path.join(repoRoot, 'named-plan.json');
const defaultPlanPath = path.join(repoRoot, 'default-plan.json');
fs.writeFileSync(namedPlanPath, JSON.stringify({
sessionName: 'Workflow Visual Proof',
repoRoot
}));
fs.writeFileSync(defaultPlanPath, JSON.stringify({ repoRoot }));
try {
const namedPlan = resolveSnapshotTarget(namedPlanPath, repoRoot);
assert.strictEqual(namedPlan.sessionName, 'workflow-visual-proof');
assert.ok(namedPlan.coordinationDir.endsWith(path.join('.orchestration', 'workflow-visual-proof')));
const defaultPlan = resolveSnapshotTarget(defaultPlanPath, repoRoot);
assert.strictEqual(defaultPlan.sessionName, 'my-repo');
assert.ok(defaultPlan.coordinationDir.endsWith(path.join('.orchestration', 'my-repo')));
} finally {
fs.rmSync(tempRoot, { recursive: true, force: true });
}
});
test('resolveSnapshotTarget rejects malformed plan files and invalid config fields', () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-orch-target-'));
const repoRoot = path.join(tempRoot, 'repo');
fs.mkdirSync(repoRoot, { recursive: true });
const invalidJsonPath = path.join(repoRoot, 'invalid-json.json');
const blankFieldsPath = path.join(repoRoot, 'blank-fields.json');
const invalidSessionNamePath = path.join(repoRoot, 'invalid-session.json');
const invalidRepoRootPath = path.join(repoRoot, 'invalid-repo-root.json');
const invalidCoordinationRootPath = path.join(repoRoot, 'invalid-coordination-root.json');
fs.writeFileSync(invalidJsonPath, '{not valid json');
fs.writeFileSync(blankFieldsPath, JSON.stringify({
sessionName: ' ',
repoRoot: ' ',
coordinationRoot: ' '
}));
fs.writeFileSync(invalidSessionNamePath, JSON.stringify({
sessionName: 42,
repoRoot
}));
fs.writeFileSync(invalidRepoRootPath, JSON.stringify({
sessionName: 'workflow',
repoRoot: ['not-a-string']
}));
fs.writeFileSync(invalidCoordinationRootPath, JSON.stringify({
sessionName: 'workflow',
repoRoot,
coordinationRoot: false
}));
try {
const blankFields = resolveSnapshotTarget(blankFieldsPath, repoRoot);
assert.strictEqual(blankFields.sessionName, 'repo');
assert.strictEqual(blankFields.repoRoot, repoRoot);
assert.ok(blankFields.coordinationDir.endsWith(path.join('.orchestration', 'repo')));
assert.throws(
() => resolveSnapshotTarget(invalidJsonPath, repoRoot),
/Invalid orchestration plan JSON/
);
assert.throws(
() => resolveSnapshotTarget(invalidSessionNamePath, repoRoot),
/sessionName must be a string when provided/
);
assert.throws(
() => resolveSnapshotTarget(invalidRepoRootPath, repoRoot),
/repoRoot must be a string when provided/
);
assert.throws(
() => resolveSnapshotTarget(invalidCoordinationRootPath, repoRoot),
/coordinationRoot must be a string when provided/
);
} finally {
fs.rmSync(tempRoot, { recursive: true, force: true });
}

View File

@@ -57,7 +57,7 @@ test('buildOrchestrationPlan creates worktrees, branches, and tmux commands', ()
repoRoot,
sessionName: 'Skill Audit',
baseRef: 'main',
launcherCommand: 'codex exec --cwd {worktree_path} --task-file {task_file}',
launcherCommand: 'codex exec --cwd {worktree_path_sh} --task-file {task_file_sh}',
workers: [
{ name: 'Docs A', task: 'Fix skills 1-4' },
{ name: 'Docs B', task: 'Fix skills 5-8' }
@@ -137,6 +137,46 @@ test('buildOrchestrationPlan normalizes global and worker seed paths', () => {
]);
});
test('buildOrchestrationPlan rejects worker names that collapse to the same slug', () => {
assert.throws(
() => buildOrchestrationPlan({
repoRoot: '/tmp/ecc',
sessionName: 'duplicates',
launcherCommand: 'echo run',
workers: [
{ name: 'Docs A', task: 'Fix skill docs' },
{ name: 'Docs/A', task: 'Fix tests' }
]
}),
/unique slugs/
);
});
test('buildOrchestrationPlan exposes shell-safe launcher aliases alongside raw defaults', () => {
const repoRoot = path.join('/tmp', 'My Repo');
const plan = buildOrchestrationPlan({
repoRoot,
sessionName: 'Spacing Audit',
launcherCommand: 'bash {repo_root_sh}/scripts/orchestrate-codex-worker.sh {task_file_sh} {handoff_file_sh} {status_file_sh} {worker_name_sh} {worker_name}',
workers: [{ name: 'Docs Fixer', task: 'Update docs' }]
});
const quote = value => `'${String(value).replace(/'/g, `'\\''`)}'`;
const resolvedRepoRoot = plan.workerPlans[0].repoRoot;
assert.ok(
plan.workerPlans[0].launchCommand.includes(`bash ${quote(resolvedRepoRoot)}/scripts/orchestrate-codex-worker.sh`),
'repo_root_sh should provide a shell-safe path'
);
assert.ok(
plan.workerPlans[0].launchCommand.includes(quote(plan.workerPlans[0].taskFilePath)),
'task_file_sh should provide a shell-safe path'
);
assert.ok(
plan.workerPlans[0].launchCommand.includes(`${quote(plan.workerPlans[0].workerName)} ${plan.workerPlans[0].workerName}`),
'raw defaults should remain available alongside shell-safe aliases'
);
});
test('normalizeSeedPaths rejects paths outside the repo root', () => {
assert.throws(
() => normalizeSeedPaths(['../outside.txt'], '/tmp/ecc'),
@@ -144,6 +184,17 @@ test('normalizeSeedPaths rejects paths outside the repo root', () => {
);
});
test('normalizeSeedPaths rejects repo root and git metadata paths', () => {
assert.throws(
() => normalizeSeedPaths(['.'], '/tmp/ecc'),
/must not target the repo root/
);
assert.throws(
() => normalizeSeedPaths(['.git/config'], '/tmp/ecc'),
/must not target git metadata/
);
});
test('materializePlan keeps worker instructions inside the worktree boundary', () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-orchestrator-test-'));
@@ -152,18 +203,25 @@ test('materializePlan keeps worker instructions inside the worktree boundary', (
repoRoot: tempRoot,
coordinationRoot: path.join(tempRoot, '.claude', 'orchestration'),
sessionName: 'Workflow E2E',
launcherCommand: 'bash {repo_root}/scripts/orchestrate-codex-worker.sh {task_file} {handoff_file} {status_file}',
launcherCommand: 'bash {repo_root_sh}/scripts/orchestrate-codex-worker.sh {task_file_sh} {handoff_file_sh} {status_file_sh}',
workers: [{ name: 'Docs', task: 'Update the workflow docs.' }]
});
materializePlan(plan);
const taskFile = fs.readFileSync(plan.workerPlans[0].taskFilePath, 'utf8');
const handoffFile = fs.readFileSync(plan.workerPlans[0].handoffFilePath, 'utf8');
assert.ok(
taskFile.includes('Report results in your final response.'),
'Task file should tell the worker to report in stdout'
);
assert.ok(
taskFile.includes('## Summary') &&
taskFile.includes('## Validation') &&
taskFile.includes('## Remaining Risks'),
'Task file should require parser-compatible headings'
);
assert.ok(
taskFile.includes('Do not spawn subagents or external agents for this task.'),
'Task file should keep nested workers single-session'
@@ -176,6 +234,18 @@ test('materializePlan keeps worker instructions inside the worktree boundary', (
!taskFile.includes('Update `'),
'Task file should not instruct the nested worker to update orchestration status files'
);
assert.ok(
handoffFile.includes('## Summary') &&
handoffFile.includes('## Validation') &&
handoffFile.includes('## Remaining Risks'),
'Handoff placeholder should seed parser-compatible headings'
);
assert.ok(
!handoffFile.includes('## Files Changed') &&
!handoffFile.includes('## Tests / Verification') &&
!handoffFile.includes('## Follow-ups'),
'Handoff placeholder should not use legacy headings'
);
} finally {
fs.rmSync(tempRoot, { recursive: true, force: true });
}

View File

@@ -0,0 +1,89 @@
'use strict';
const assert = require('assert');
const { parseArgs } = require('../../scripts/orchestration-status');
console.log('=== Testing orchestration-status.js ===\n');
let passed = 0;
let failed = 0;
function test(desc, fn) {
try {
fn();
console.log(`${desc}`);
passed++;
} catch (error) {
console.log(`${desc}: ${error.message}`);
failed++;
}
}
test('parseArgs reads a target with an optional write path', () => {
assert.deepStrictEqual(
parseArgs([
'node',
'scripts/orchestration-status.js',
'workflow-visual-proof',
'--write',
'/tmp/snapshot.json'
]),
{
target: 'workflow-visual-proof',
writePath: '/tmp/snapshot.json'
}
);
});
test('parseArgs does not treat the write path as the target', () => {
assert.deepStrictEqual(
parseArgs([
'node',
'scripts/orchestration-status.js',
'--write',
'/tmp/snapshot.json',
'workflow-visual-proof'
]),
{
target: 'workflow-visual-proof',
writePath: '/tmp/snapshot.json'
}
);
});
test('parseArgs rejects missing write values and unknown flags', () => {
assert.throws(
() => parseArgs([
'node',
'scripts/orchestration-status.js',
'workflow-visual-proof',
'--write'
]),
/--write requires an output path/
);
assert.throws(
() => parseArgs([
'node',
'scripts/orchestration-status.js',
'workflow-visual-proof',
'--unknown'
]),
/Unknown flag/
);
});
test('parseArgs rejects multiple positional targets', () => {
assert.throws(
() => parseArgs([
'node',
'scripts/orchestration-status.js',
'first',
'second'
]),
/Expected a single session name or plan path/
);
});
console.log(`\n=== Results: ${passed} passed, ${failed} failed ===`);
if (failed > 0) process.exit(1);