mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-09 03:39:51 -10:00
fix(slice-05): CLI install/create timeout bump + inspect cross-primitive surfacing (QA-20260601 A1+C1 repair) (OPR.0.3.2.5)
Per orch dispatch + velocity-qa REPORT 2026-06-01 against tip 233dd16d:
A1 BLOCKING fix + C1 CONCERN fix bundled (same surface; orch ruling).
A1 (BLOCKING) — CLI 5s timeout vs real install duration:
- `rig bundle install` hit DaemonConnectionError after 5000ms while
daemon completed the mutating install successfully (history audit
shows outcome:success; 4 tmux sessions launched). Operator-unsafe
retry path (rig_name_collision on second attempt) per QA evidence
bundle-install-timeout-repro.txt.
- Root cause: DaemonClient default timeoutMs=5_000 (client.ts:31).
/install is a long-running operation: bootstrap orchestrator +
per-kind sibling-primitive vendoring (skills/plugins/workflow_specs/
context_packs/agent_images) + integrity hashing + tmux session
launches. 5s was wrong by design for this surface.
- Fix: bump per-call timeoutMs to 120_000ms (2min) on both /install
AND /create (create has the same long-running shape under the 7.5
vendoring path). Comment explicitly notes if a bundle author hits
this ceiling, async-completable /bundles/jobs/<id> is the
follow-on (NOT in slice-05 scope).
- packages/cli/src/commands/bundle.ts +2 timeoutMs args (install:160,
create:88) — surgical 1-arg-per-call.
C1 (CONCERN, bundled per orch) — /inspect normalizer dropped cross-
primitive fields:
- v2 archive built via Checkpoint 7.5 carries skills/plugins/
workflow_specs/context_packs/agent_images in the built bundle.yaml
(QA confirmed). But /api/bundles/inspect's normalized response
manifest only carried name/version/createdAt/rigSpec/agents/
integrity/provenance/compatibility — cross-primitive fields STRIPPED
out by the inspect normalizer.
- Fix: extend the v2 inspect normalized manifest to surface 5 cross-
primitive blocks as camelCase fields (matches v1 normalizer shape
which already carries them). 1-function pass-through addition.
- packages/daemon/src/routes/bundles.ts +21 lines in the /inspect v2
branch (lines ~1054 area).
New integration test in bundle-routes.test.ts (+78 lines):
"POST /api/bundles/inspect surfaces cross-primitive fields for a v2
archive built from author bundle.yaml" — builds a v2 bundle with
skills + plugins via Checkpoint 7.5's auto-detect path, then inspects
the built archive, asserts manifest.skills + manifest.plugins are
both present with the expected values.
Discriminator-flip evidence (PROACTIVELY for C1 — the testable surface):
removed the 5 cross-primitive field assignments in the /inspect v2
normalizer. Result: 1/57 fail — exactly the new inspect cross-primitive
test. Restored, 57/57 across bundle-routes.test.ts + 232/232 across
full slice-05 daemon suite.
A1 discriminator-flip honest disclosure: the CLI timeoutMs bump is a
constant change (5_000 → 120_000) with no automated test. Manual proof
= QA's repro at qa-evidence dir + the absence of timeout-class repros
in the new dogfood pass after this bump. Adding a slow-fetchImpl mock
test would be defensive but is bounded follow-up scope (skipped per
banked finalization-means-complete-specced — the fix itself is one
constant change + the manual repro is the authoritative proof).
After guard CLEAR: re-route to velocity-qa per banked qa-fix-lands-
post-guard for re-verify at new tip; QA reruns the install repro
against the bumped timeout + verifies inspect now surfaces fields.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
1 parent
233dd16d72
commit
dc4df12fc6
3 files changed
+106
-2
No files matched your search
@@ -78,13 +78,22 @@ export function bundleCommand(depsOverride?: StatusDeps): Command {
|
||||
if (opts.minCliVersion) compatibility.minCliVersion = opts.minCliVersion;
|
||||
const hasCompatibility = Object.keys(compatibility).length > 0;
|
||||
|
||||
// QA-20260601 A1 repair (banked banked-create-install-long-running):
|
||||
// /create can take seconds-to-minutes (pod-aware assembler walk +
|
||||
// Item-6 cross-primitive vendoring + integrity-over-all-content +
|
||||
// tar pack). Default 5000ms CLI timeout (client.ts:31) is too short
|
||||
// for real-world bundles; daemon completes work but CLI reports
|
||||
// failure. Per-call 120s upper bound covers reasonable real-world
|
||||
// bundle sizes; if a bundle author hits this, the daemon-side
|
||||
// operation is async-completable in a future /bundles/jobs/<id>
|
||||
// surface (not in slice-05 scope).
|
||||
const res = await client.post<Record<string, unknown>>("/api/bundles/create", {
|
||||
specPath: spec, bundleName: opts.name, bundleVersion: opts.bundleVersion, outputPath: opts.output,
|
||||
includePackages: opts.includePackages,
|
||||
rigRoot: opts.rigRoot ? nodePath.resolve(opts.rigRoot) : undefined,
|
||||
provenance: buildClientProvenance(opts.notes),
|
||||
...(hasCompatibility ? { compatibility } : {}),
|
||||
});
|
||||
}, { timeoutMs: 120_000 });
|
||||
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify(res.data));
|
||||
@@ -148,6 +157,12 @@ export function bundleCommand(depsOverride?: StatusDeps): Command {
|
||||
const client = await getClient(deps);
|
||||
if (!client) { process.exitCode = 1; return; }
|
||||
|
||||
// QA-20260601 A1 repair: /install completes a full bootstrap run
|
||||
// (resolve → vendor sibling primitives → boot rig sessions) which
|
||||
// can take many seconds for real bundles. Default 5000ms CLI
|
||||
// timeout (client.ts:31) caused CLI to report failure while
|
||||
// daemon completed the mutating install — operator-unsafe retry
|
||||
// path (rig_name_collision on second attempt). Bumped to 120s.
|
||||
const res = await client.post<Record<string, unknown>>("/api/bundles/install", {
|
||||
bundlePath, plan: opts.plan ?? false, autoApprove: opts.yes ?? false, targetRoot: opts.target,
|
||||
// Item 2 / slice-05 Checkpoint 3.3: send CLI version + skip flag for the
|
||||
@@ -158,7 +173,7 @@ export function bundleCommand(depsOverride?: StatusDeps): Command {
|
||||
// Item 3 / slice-05 Checkpoint 4.2: send force flag for the daemon-side
|
||||
// install-time conflict check. Operator-explicit override only.
|
||||
force: opts.force ?? false,
|
||||
});
|
||||
}, { timeoutMs: 120_000 });
|
||||
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify(res.data));
|
||||
|
||||
@@ -1051,6 +1051,26 @@ bundleRoutes.post("/inspect", async (c) => {
|
||||
// (same single-contract reason as provenance above). v1 already
|
||||
// surfaces via the normalizer at the end of this handler.
|
||||
compatibility: normalizeCompatibilityBlock(rawParsed["compatibility"]),
|
||||
// Item 6 / Checkpoint 7.5 / QA-20260601 C1 repair: surface the 5
|
||||
// cross-primitive blocks normalized to camelCase so /inspect's
|
||||
// contract carries the same shape v1's normalizer already
|
||||
// surfaces. Raw YAML keys are snake_case; expose camelCase to
|
||||
// match the rest of the v2 inspect contract.
|
||||
skills: Array.isArray(rawParsed["skills"])
|
||||
? (rawParsed["skills"] as unknown[]).filter((s): s is string => typeof s === "string")
|
||||
: undefined,
|
||||
plugins: Array.isArray(rawParsed["plugins"])
|
||||
? (rawParsed["plugins"] as Array<Record<string, unknown>>).filter((p) => p && typeof p === "object")
|
||||
: undefined,
|
||||
workflowSpecs: Array.isArray(rawParsed["workflow_specs"])
|
||||
? (rawParsed["workflow_specs"] as unknown[]).filter((s): s is string => typeof s === "string")
|
||||
: undefined,
|
||||
contextPacks: Array.isArray(rawParsed["context_packs"])
|
||||
? (rawParsed["context_packs"] as unknown[]).filter((s): s is string => typeof s === "string")
|
||||
: undefined,
|
||||
agentImages: Array.isArray(rawParsed["agent_images"])
|
||||
? (rawParsed["agent_images"] as unknown[]).filter((s): s is string => typeof s === "string")
|
||||
: undefined,
|
||||
};
|
||||
const integrityCompat = integritySection ? {
|
||||
schemaVersion: 2,
|
||||
|
||||
@@ -1648,6 +1648,75 @@ describe("Bundle API routes", () => {
|
||||
}
|
||||
});
|
||||
|
||||
// Item 6 / QA-20260601 C1 repair: /api/bundles/inspect normalized
|
||||
// response on a v2 archive containing cross-primitive blocks must
|
||||
// surface skills + plugins + workflowSpecs + contextPacks +
|
||||
// agentImages alongside provenance + compatibility. Built bundle.yaml
|
||||
// contains the snake_case fields (Checkpoint 7.5); inspect's response
|
||||
// now mirrors them as camelCase per the v2 normalized-manifest contract.
|
||||
it("POST /api/bundles/inspect surfaces cross-primitive fields for a v2 archive built from author bundle.yaml", async () => {
|
||||
const sourceRoot = fs.mkdtempSync(path.join(os.tmpdir(), "inspect-crossprim-src-"));
|
||||
const outputPath = path.join(tmpDir, "inspect-crossprim.rigbundle");
|
||||
try {
|
||||
fs.mkdirSync(path.join(sourceRoot, "agents/impl"), { recursive: true });
|
||||
fs.writeFileSync(path.join(sourceRoot, "agents/impl/agent.yaml"), [
|
||||
'name: impl-agent', 'version: "1.0.0"', 'resources:', ' skills: []',
|
||||
'profiles:', ' default:', ' uses:', ' skills: []',
|
||||
].join("\n"));
|
||||
const specPath = path.join(sourceRoot, "rig.yaml");
|
||||
fs.writeFileSync(specPath, [
|
||||
'version: "0.2"', 'name: inspect-crossprim-rig', 'pods:',
|
||||
' - id: dev', ' label: Dev', ' members:',
|
||||
' - id: impl', ' agent_ref: "local:agents/impl"',
|
||||
' profile: default', ' runtime: claude-code', ' cwd: .',
|
||||
' edges: []', 'edges: []',
|
||||
].join("\n"));
|
||||
// Just plugins + skills for compactness; consumer fields are the
|
||||
// same shape across all 5 kinds via the inspect normalization.
|
||||
fs.mkdirSync(path.join(sourceRoot, "skills/inspect-skill"), { recursive: true });
|
||||
fs.writeFileSync(path.join(sourceRoot, "skills/inspect-skill/SKILL.md"), "# inspect skill");
|
||||
fs.mkdirSync(path.join(sourceRoot, "plugins/inspect-plugin"), { recursive: true });
|
||||
fs.writeFileSync(path.join(sourceRoot, "plugins/inspect-plugin/plugin.json"), '{"name":"inspect-plugin","version":"1.0"}');
|
||||
fs.writeFileSync(path.join(sourceRoot, "bundle.yaml"), [
|
||||
'skills:',
|
||||
' - skills/inspect-skill/SKILL.md',
|
||||
'plugins:',
|
||||
' - id: inspect-plugin',
|
||||
' source:',
|
||||
' kind: local',
|
||||
' path: plugins/inspect-plugin',
|
||||
].join("\n"));
|
||||
|
||||
const createRes = await app.request("/api/bundles/create", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
specPath, rigRoot: sourceRoot,
|
||||
bundleName: "inspect-crossprim-test", bundleVersion: "0.1.0", outputPath,
|
||||
}),
|
||||
});
|
||||
expect(createRes.status).toBe(201);
|
||||
|
||||
const inspectRes = await app.request("/api/bundles/inspect", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ bundlePath: outputPath }),
|
||||
});
|
||||
expect(inspectRes.status).toBe(200);
|
||||
const inspectBody = await inspectRes.json();
|
||||
// QA-C1 repair: cross-primitive fields surfaced in the normalized
|
||||
// manifest. v2 archive → response.manifest carries camelCase keys.
|
||||
expect(inspectBody.manifest.skills).toBeDefined();
|
||||
expect(inspectBody.manifest.skills).toEqual(["skills/inspect-skill/SKILL.md"]);
|
||||
expect(inspectBody.manifest.plugins).toBeDefined();
|
||||
expect(inspectBody.manifest.plugins).toHaveLength(1);
|
||||
expect(inspectBody.manifest.plugins[0].id).toBe("inspect-plugin");
|
||||
} finally {
|
||||
fs.rmSync(sourceRoot, { recursive: true, force: true });
|
||||
if (fs.existsSync(outputPath)) fs.rmSync(outputPath);
|
||||
}
|
||||
});
|
||||
|
||||
// Item 6 / Checkpoint 7.5 B1 repair (banked 79a89d40 guard catch):
|
||||
// symlink escape via author bundle.yaml — FILE shape. A skill path
|
||||
// that looks lexically contained but symlinks to an outside file must
|
||||
|
||||
Reference in new issue
Block a user