Merge pull request #932 from open-gsd/hotfix/1.4.2
chore: merge release v1.4.2 to main
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "gsd-core",
|
||||
"displayName": "GSD Core",
|
||||
"version": "1.4.1",
|
||||
"version": "1.4.2",
|
||||
"description": "GSD Core is a meta-prompting, context engineering, and spec-driven development system for AI coding agents.",
|
||||
"author": {
|
||||
"name": "open-gsd",
|
||||
|
||||
@@ -6,6 +6,13 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [1.4.2] - 2026-06-09
|
||||
|
||||
### Fixed
|
||||
|
||||
- **`/gsd-plan-phase`, `/gsd-execute-phase`, `/gsd-autonomous` no longer carry `context: fork`** — these are spawning orchestrators; a forked subagent context has no `Agent` tool, preventing them from spawning the subagents they require. `effort: xhigh` is preserved. Fixes `/gsd:autonomous` halting with "running as a forked subagent" on 1.4.1 (#921). Also replaces the introspection-based Agent-availability check in `plan-phase`'s `<runtime_compatibility>` block with an attempt-based gate: the workflow now always attempts the `Agent()` call and only stops if a real tool-unavailable error is returned, eliminating false-negative aborts in top-level sessions (#922). (#921)
|
||||
- **`gsd-context-monitor.js` now echoes the actual invoking hook event name** — instead of hardcoding `hookEventName: "PostToolUse"` (or `"AfterTool"` for Gemini), the hook reads `data.hook_event_name` from the stdin payload and falls back to the runtime heuristic only when the field is absent or blank; this fixes Claude Code rejecting hook output with `"expected Stop but got PostToolUse"` when the monitor is invoked by the Stop, SubagentStop, or PreCompact hooks registered in PR #821. (#925) (#926)
|
||||
|
||||
## [1.4.1] - 2026-06-09
|
||||
|
||||
### Changed
|
||||
@@ -20,7 +27,7 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||
- **`init execute-phase` and `cmdCommit` now produce correct `branch_name` when `project_code` is set** — the `{phase}` substitution in `phase_branch_template` now calls `normalizePhaseName()`, stripping the project-code prefix and zero-padding the number, so the generated branch is e.g. `gsd/phase-01-foundation` instead of `gsd/phase-CK-01-foundation`. Both the execute-phase output path (`src/init.cts`) and the pre-execution commit path (`src/commands.cts`) are fixed. (#904) (#904)
|
||||
- **`syncStateFrontmatter` no longer strips `current_phase`, `current_phase_name`, `current_plan`, and `progress` from `STATE.md`** — when body annotations are absent (e.g. after an agent rewrites the body), the existing frontmatter values for those scalars are now preserved, mirroring the fallback already applied in `cmdStateJson`. (#905) (#905)
|
||||
- **Top-level Claude Code `/gsd-plan-phase` now always spawns the researcher/planner/plan-checker agents instead of collapsing them inline** — a `<runtime_compatibility>` block after `</available_agent_types>` makes the Agent-availability requirement explicit and documents that the workflow fails-closed (stops with a clear log message) in genuinely Agent-less contexts; seven "ORCHESTRATOR RULE — CODEX RUNTIME" labels are renamed to "ALL RUNTIMES" so the guard applies universally; `execute-phase.md` scopes its existing "Other runtimes" inline-fallback prose to non-Claude contexts, preserving the #853 backgrounded-agent behaviour. (#913) (#913)
|
||||
- **`/gsd-manager` and `/gsd-autonomous --interactive` no longer silently skip worktree isolation and independent verification on Claude Code.** They dispatched plan/execute as background agents, but a backgrounded Claude Code agent has no Agent/Task tool and cannot spawn the nested executors, plan-checker, or verifier — so isolation and verification silently never ran. Both workflows now resolve the runtime and run plan/execute inline on Claude Code (background dispatch is kept on runtimes that support nested subagents). (#863)
|
||||
- **`/gsd-manager` and `/gsd-autonomous --interactive` no longer silently skip worktree isolation and independent verification on Claude Code.** They dispatched plan/execute as background agents, but a backgrounded Claude Code agent has no Agent/Task tool and cannot spawn the nested executors, plan-checker, or verifier — so isolation and verification silently never ran. Both workflows now resolve the runtime and run plan/execute inline on Claude Code; background dispatch is kept on runtimes that support nested subagents. (#863)
|
||||
- **Installer no longer leaks `gsd-cmd-rewrites-*` temp directories.** Each install that emitted slash commands left one `fs.mkdtempSync` directory under the system temp root; on `tmpfs` `/tmp` hosts these accumulated and consumed RAM-backed storage. `installRuntimeArtifacts()` now removes the temp copy in a `finally` once command files are copied. (#862)
|
||||
- Corrected the installer `--help` profile skill counts: `core` now shows 8 (was 7) and `standard` shows 14 (was 13), both derived from `PROFILES` so they can't drift again; the `full` line drops the stale hardcoded `66` for `all skills`. (#834) (#847)
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
name: gsd:autonomous
|
||||
description: Run all remaining phases autonomously — discuss→plan→execute per phase
|
||||
argument-hint: "[--from N] [--to N] [--only N] [--interactive]"
|
||||
context: fork
|
||||
effort: xhigh
|
||||
allowed-tools:
|
||||
- Read
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
name: gsd:execute-phase
|
||||
description: Execute all plans in a phase with wave-based parallelization
|
||||
argument-hint: "<phase-number> [--wave N] [--gaps-only] [--interactive] [--tdd]"
|
||||
context: fork
|
||||
effort: xhigh
|
||||
allowed-tools:
|
||||
- Read
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
name: gsd:plan-phase
|
||||
description: Create detailed phase plan (PLAN.md) with verification loop
|
||||
argument-hint: "[phase] [--auto] [--research] [--skip-research] [--research-phase <N>] [--view] [--gaps] [--skip-verify] [--prd <file>] [--ingest <path-or-glob>] [--ingest-format <auto|nygard|madr|narrative>] [--reviews] [--text] [--tdd] [--mvp]"
|
||||
context: fork
|
||||
effort: xhigh
|
||||
allowed-tools:
|
||||
- Read
|
||||
|
||||
@@ -14,7 +14,7 @@ The hyphen and colon forms are *runtime-specific spellings of the same command*.
|
||||
|
||||
### Skill Runtime Behavior (Claude Code)
|
||||
|
||||
Heavy workflow skills (`/gsd-plan-phase`, `/gsd-execute-phase`, `/gsd-autonomous`) carry `context: fork` in their frontmatter. On Claude Code, this runs each skill in an isolated subagent context window, protecting the main session's context budget. The skills also declare `effort: xhigh`, signalling maximum token budget to the runtime.
|
||||
Heavy workflow skills (`/gsd-plan-phase`, `/gsd-execute-phase`, `/gsd-autonomous`) declare `effort: xhigh`, signalling maximum token budget to the runtime. These skills are spawning orchestrators — they must run at top level so they retain the `Agent` tool needed to spawn subagents. They do **not** carry `context: fork` (see #921).
|
||||
|
||||
Quick-status skills (`/gsd-progress`, `/gsd-stats`) declare `effort: low`, directing the runtime to use a minimal token budget for fast reads.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gsd-core",
|
||||
"version": "1.4.1",
|
||||
"version": "1.4.2",
|
||||
"description": "GSD Core — a meta-prompting, context engineering, and spec-driven development system for AI coding agents. Loads gsd's operating context into every Gemini CLI session.",
|
||||
"contextFileName": "GEMINI.md"
|
||||
}
|
||||
|
||||
@@ -39,10 +39,13 @@ discuss-phase early-exit path. It does NOT authorize inline role performance for
|
||||
plan-phase agents.
|
||||
|
||||
**Other runtimes:**
|
||||
If the Agent tool is genuinely absent (e.g. a backgrounded Claude Code agent per
|
||||
#853, or a non-Claude runtime that does not expose Agent/agent), log the gap and
|
||||
stop — do NOT perform researcher/planner/checker roles inline. Independent agent
|
||||
contexts are required for the plan-checker gate to be meaningful.
|
||||
Do not pre-judge Agent availability by introspection. Always attempt the actual
|
||||
Agent() call for gsd-phase-researcher, gsd-planner, and gsd-plan-checker. Only
|
||||
a real tool-unavailable error returned by Agent() is a reliable absence signal —
|
||||
never stop based on a self-assessed "I think Agent is unavailable." If the call
|
||||
fails with a tool-unavailable error, log the gap and stop — do NOT collapse
|
||||
researcher/planner/checker roles inline. Independent agent contexts are required
|
||||
for the plan-checker gate to be meaningful.
|
||||
</runtime_compatibility>
|
||||
|
||||
<process>
|
||||
|
||||
@@ -182,7 +182,8 @@ process.stdin.on('end', () => {
|
||||
|
||||
const output = {
|
||||
hookSpecificOutput: {
|
||||
hookEventName: process.env.GEMINI_API_KEY ? "AfterTool" : "PostToolUse",
|
||||
hookEventName: (data.hook_event_name && data.hook_event_name.trim())
|
||||
|| (process.env.GEMINI_API_KEY ? "AfterTool" : "PostToolUse"),
|
||||
additionalContext: message
|
||||
}
|
||||
};
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@opengsd/gsd-core",
|
||||
"version": "1.4.1",
|
||||
"version": "1.4.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@opengsd/gsd-core",
|
||||
"version": "1.4.1",
|
||||
"version": "1.4.2",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.2.84",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opengsd/gsd-core",
|
||||
"version": "1.4.1",
|
||||
"version": "1.4.2",
|
||||
"description": "GSD Core is a meta-prompting, context engineering, and spec-driven development system for AI coding agents.",
|
||||
"bin": {
|
||||
"gsd-core": "bin/install.js",
|
||||
|
||||
205
tests/bug-925-context-monitor-hook-event-name.test.cjs
Normal file
205
tests/bug-925-context-monitor-hook-event-name.test.cjs
Normal file
@@ -0,0 +1,205 @@
|
||||
/**
|
||||
* Regression test for bug #925
|
||||
*
|
||||
* hooks/gsd-context-monitor.js hardcodes `hookEventName: "PostToolUse"` (or
|
||||
* "AfterTool" for Gemini) regardless of which hook event invoked it. Since
|
||||
* PR #821 the same script is also registered under Stop, SubagentStop, and
|
||||
* PreCompact in hooks/hooks.json. Claude Code rejects output whose
|
||||
* hookSpecificOutput.hookEventName doesn't echo the triggering event:
|
||||
*
|
||||
* "expected Stop but got PostToolUse"
|
||||
*
|
||||
* Fix: derive hookEventName from the parsed stdin payload's `hook_event_name`
|
||||
* field (already available in the data object), falling back to the
|
||||
* Gemini / non-Gemini heuristic for runtimes that don't send it.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const { test, describe } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const { execFileSync } = require('node:child_process');
|
||||
|
||||
const MONITOR_PATH = path.join(__dirname, '..', 'hooks', 'gsd-context-monitor.js');
|
||||
|
||||
/**
|
||||
* Write a bridge metrics file and invoke the context monitor with the given
|
||||
* payload fields. Returns the parsed stdout object (or null if the hook
|
||||
* produced no output).
|
||||
*
|
||||
* remainingPct must be <= 35 to cross the WARNING threshold so the hook
|
||||
* actually emits output.
|
||||
*/
|
||||
function runMonitor({ hookEventName, sessionId, remainingPct = 30, usedPct = 70, env = {} }) {
|
||||
const bridgePath = path.join(os.tmpdir(), `claude-ctx-${sessionId}.json`);
|
||||
fs.writeFileSync(bridgePath, JSON.stringify({
|
||||
session_id: sessionId,
|
||||
remaining_percentage: remainingPct,
|
||||
used_pct: usedPct,
|
||||
timestamp: Math.floor(Date.now() / 1000),
|
||||
}));
|
||||
|
||||
const payload = { session_id: sessionId, cwd: os.tmpdir() };
|
||||
if (hookEventName !== undefined) {
|
||||
payload.hook_event_name = hookEventName;
|
||||
}
|
||||
|
||||
let stdout = '';
|
||||
try {
|
||||
stdout = execFileSync(process.execPath, [MONITOR_PATH], {
|
||||
input: JSON.stringify(payload),
|
||||
encoding: 'utf-8',
|
||||
timeout: 5000,
|
||||
env: { ...process.env, ...env },
|
||||
});
|
||||
} catch (e) {
|
||||
stdout = e.stdout || '';
|
||||
} finally {
|
||||
try { fs.unlinkSync(bridgePath); } catch { /* noop */ }
|
||||
try {
|
||||
fs.unlinkSync(path.join(os.tmpdir(), `claude-ctx-${sessionId}-warned.json`));
|
||||
} catch { /* noop */ }
|
||||
}
|
||||
|
||||
if (!stdout) return null;
|
||||
return JSON.parse(stdout);
|
||||
}
|
||||
|
||||
function makeSessionId(suffix) {
|
||||
return `test-925-${suffix}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
||||
}
|
||||
|
||||
// ─── hookEventName echoing ────────────────────────────────────────────────────
|
||||
|
||||
describe('bug #925: context monitor echoes the invoking hook event name', () => {
|
||||
test('hookEventName is "Stop" when payload contains hook_event_name: "Stop"', () => {
|
||||
const out = runMonitor({ hookEventName: 'Stop', sessionId: makeSessionId('stop') });
|
||||
assert.ok(out, 'hook must emit output when context is below WARNING threshold (remaining=30)');
|
||||
assert.strictEqual(
|
||||
out.hookSpecificOutput?.hookEventName,
|
||||
'Stop',
|
||||
`Expected hookEventName "Stop" but got "${out.hookSpecificOutput?.hookEventName}". ` +
|
||||
'The hook must echo the hook_event_name from stdin, not hardcode "PostToolUse".'
|
||||
);
|
||||
});
|
||||
|
||||
test('hookEventName is "SubagentStop" when payload contains hook_event_name: "SubagentStop"', () => {
|
||||
const out = runMonitor({ hookEventName: 'SubagentStop', sessionId: makeSessionId('subagent-stop') });
|
||||
assert.ok(out, 'hook must emit output when context is below WARNING threshold');
|
||||
assert.strictEqual(
|
||||
out.hookSpecificOutput?.hookEventName,
|
||||
'SubagentStop',
|
||||
`Expected hookEventName "SubagentStop" but got "${out.hookSpecificOutput?.hookEventName}".`
|
||||
);
|
||||
});
|
||||
|
||||
test('hookEventName is "PreCompact" when payload contains hook_event_name: "PreCompact"', () => {
|
||||
const out = runMonitor({ hookEventName: 'PreCompact', sessionId: makeSessionId('precompact') });
|
||||
assert.ok(out, 'hook must emit output when context is below WARNING threshold');
|
||||
assert.strictEqual(
|
||||
out.hookSpecificOutput?.hookEventName,
|
||||
'PreCompact',
|
||||
`Expected hookEventName "PreCompact" but got "${out.hookSpecificOutput?.hookEventName}".`
|
||||
);
|
||||
});
|
||||
|
||||
test('hookEventName is "PostToolUse" when payload contains hook_event_name: "PostToolUse"', () => {
|
||||
const out = runMonitor({ hookEventName: 'PostToolUse', sessionId: makeSessionId('posttools') });
|
||||
assert.ok(out, 'hook must emit output when context is below WARNING threshold');
|
||||
assert.strictEqual(
|
||||
out.hookSpecificOutput?.hookEventName,
|
||||
'PostToolUse',
|
||||
`Expected hookEventName "PostToolUse" but got "${out.hookSpecificOutput?.hookEventName}".`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Fallback behaviour (no hook_event_name in payload) ──────────────────────
|
||||
|
||||
describe('bug #925: context monitor falls back to heuristic when hook_event_name absent', () => {
|
||||
test('falls back to "PostToolUse" when hook_event_name is absent (non-Gemini)', () => {
|
||||
const env = { ...process.env };
|
||||
delete env.GEMINI_API_KEY;
|
||||
const out = runMonitor({
|
||||
hookEventName: undefined,
|
||||
sessionId: makeSessionId('fallback-non-gemini'),
|
||||
env: { GEMINI_API_KEY: '' }, // ensure unset
|
||||
});
|
||||
assert.ok(out, 'hook must emit output when context is below WARNING threshold');
|
||||
assert.strictEqual(
|
||||
out.hookSpecificOutput?.hookEventName,
|
||||
'PostToolUse',
|
||||
`Expected fallback "PostToolUse" for non-Gemini but got "${out.hookSpecificOutput?.hookEventName}".`
|
||||
);
|
||||
});
|
||||
|
||||
test('falls back to "AfterTool" when hook_event_name is absent and GEMINI_API_KEY is set', () => {
|
||||
const out = runMonitor({
|
||||
hookEventName: undefined,
|
||||
sessionId: makeSessionId('fallback-gemini'),
|
||||
env: { GEMINI_API_KEY: 'fake-key-for-test' },
|
||||
});
|
||||
assert.ok(out, 'hook must emit output when context is below WARNING threshold');
|
||||
assert.strictEqual(
|
||||
out.hookSpecificOutput?.hookEventName,
|
||||
'AfterTool',
|
||||
`Expected fallback "AfterTool" for Gemini but got "${out.hookSpecificOutput?.hookEventName}".`
|
||||
);
|
||||
});
|
||||
|
||||
test('falls back to "PostToolUse" when hook_event_name is an empty string (non-Gemini)', () => {
|
||||
const out = runMonitor({
|
||||
hookEventName: '',
|
||||
sessionId: makeSessionId('fallback-empty'),
|
||||
env: { GEMINI_API_KEY: '' },
|
||||
});
|
||||
assert.ok(out, 'hook must emit output when context is below WARNING threshold');
|
||||
assert.strictEqual(
|
||||
out.hookSpecificOutput?.hookEventName,
|
||||
'PostToolUse',
|
||||
`Expected fallback "PostToolUse" for empty hook_event_name but got "${out.hookSpecificOutput?.hookEventName}".`
|
||||
);
|
||||
});
|
||||
|
||||
test('falls back to "PostToolUse" when hook_event_name is whitespace-only (non-Gemini)', () => {
|
||||
// trim() makes " " → "" which is falsy, so the || fallback fires
|
||||
const out = runMonitor({
|
||||
hookEventName: ' ',
|
||||
sessionId: makeSessionId('fallback-whitespace'),
|
||||
env: { GEMINI_API_KEY: '' },
|
||||
});
|
||||
assert.ok(out, 'hook must emit output when context is below WARNING threshold');
|
||||
assert.strictEqual(
|
||||
out.hookSpecificOutput?.hookEventName,
|
||||
'PostToolUse',
|
||||
`Expected fallback "PostToolUse" for whitespace-only hook_event_name but got "${out.hookSpecificOutput?.hookEventName}".`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Critical threshold also echoes the event name ───────────────────────────
|
||||
|
||||
describe('bug #925: critical threshold warning also uses correct hookEventName', () => {
|
||||
test('CRITICAL warning emitted under Stop also echoes "Stop"', () => {
|
||||
const out = runMonitor({
|
||||
hookEventName: 'Stop',
|
||||
sessionId: makeSessionId('critical-stop'),
|
||||
remainingPct: 20,
|
||||
usedPct: 80,
|
||||
});
|
||||
assert.ok(out, 'hook must emit output at critical threshold (remaining=20)');
|
||||
assert.strictEqual(
|
||||
out.hookSpecificOutput?.hookEventName,
|
||||
'Stop',
|
||||
`Expected hookEventName "Stop" at critical threshold, got "${out.hookSpecificOutput?.hookEventName}".`
|
||||
);
|
||||
assert.match(
|
||||
out.hookSpecificOutput?.additionalContext || '',
|
||||
/CONTEXT CRITICAL/,
|
||||
'Output should be a CRITICAL warning at remaining=20'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -4,20 +4,28 @@
|
||||
// transformation is asserted — not inspected for string presence.
|
||||
|
||||
/**
|
||||
* #769 — context:fork + effort: frontmatter on heavy workflow skills.
|
||||
* #769 — effort: frontmatter on heavy workflow skills.
|
||||
* #921 — spawning orchestrators must NOT carry context: fork.
|
||||
*
|
||||
* Context: context:fork was added by #769 to protect context budget, but
|
||||
* plan-phase, execute-phase, and autonomous are spawning orchestrators — a
|
||||
* forked subagent has no Agent/Task tool, breaking their core function.
|
||||
* effort: xhigh is preserved; context: fork is removed from these three.
|
||||
* The converter still passes context: fork through if a source file has it
|
||||
* (for any future leaf skill that legitimately needs isolation).
|
||||
*
|
||||
* Verifies:
|
||||
* 1. Source commands/gsd/autonomous.md has context: fork and effort: xhigh
|
||||
* 2. Source commands/gsd/execute-phase.md has context: fork and effort: xhigh
|
||||
* 3. Source commands/gsd/plan-phase.md has context: fork and effort: xhigh
|
||||
* 1. Source commands/gsd/autonomous.md does NOT have context: fork, has effort: xhigh
|
||||
* 2. Source commands/gsd/execute-phase.md does NOT have context: fork, has effort: xhigh
|
||||
* 3. Source commands/gsd/plan-phase.md does NOT have context: fork, has effort: xhigh
|
||||
* 4. Source commands/gsd/progress.md has effort: low
|
||||
* 5. Source commands/gsd/stats.md has effort: low
|
||||
* 6. Claude global install: SKILL.md for autonomous has context: fork and effort: xhigh
|
||||
* 7. Claude global install: SKILL.md for execute-phase has context: fork and effort: xhigh
|
||||
* 8. Claude global install: SKILL.md for plan-phase has context: fork and effort: xhigh
|
||||
* 6. Claude global install: SKILL.md for autonomous has effort: xhigh, NOT context: fork
|
||||
* 7. Claude global install: SKILL.md for execute-phase has effort: xhigh, NOT context: fork
|
||||
* 8. Claude global install: SKILL.md for plan-phase has effort: xhigh, NOT context: fork
|
||||
* 9. Claude global install: SKILL.md for progress has effort: low
|
||||
* 10. Claude global install: SKILL.md for stats has effort: low
|
||||
* 11. convertClaudeCommandToClaudeSkill preserves context: fork field
|
||||
* 11. convertClaudeCommandToClaudeSkill still passes context: fork through (for non-orchestrator skills)
|
||||
* 12. convertClaudeCommandToClaudeSkill preserves effort: field
|
||||
*/
|
||||
|
||||
@@ -90,11 +98,15 @@ function runClaudeGlobalInstall(claudeHome) {
|
||||
|
||||
// ─── describe 1: Source command files have correct frontmatter ────────────────
|
||||
|
||||
describe('#769 source commands: heavy skills have context: fork and effort: xhigh', () => {
|
||||
test('commands/gsd/autonomous.md has context: fork', () => {
|
||||
// #921/#922: spawning orchestrators must NOT carry context: fork — a forked
|
||||
// subagent has no Agent/Task tool, making it impossible for orchestrators to
|
||||
// spawn their required subagents. context: fork is appropriate only for leaf
|
||||
// skills that do not themselves dispatch agents. effort: xhigh is preserved.
|
||||
describe('#769/#921 source commands: spawning orchestrators have effort: xhigh but NOT context: fork', () => {
|
||||
test('commands/gsd/autonomous.md does NOT have context: fork (#921)', () => {
|
||||
const fm = readFrontmatter(path.join(SOURCE_COMMANDS_DIR, 'autonomous.md'));
|
||||
assert.match(fm, /^context:[ \t]*fork$/m,
|
||||
`autonomous.md frontmatter must have context: fork\nActual:\n${fm}`);
|
||||
assert.doesNotMatch(fm, /^context:[ \t]*fork$/m,
|
||||
`autonomous.md is a spawning orchestrator and must NOT have context: fork (#921)\nActual:\n${fm}`);
|
||||
});
|
||||
|
||||
test('commands/gsd/autonomous.md has effort: xhigh', () => {
|
||||
@@ -103,10 +115,10 @@ describe('#769 source commands: heavy skills have context: fork and effort: xhig
|
||||
`autonomous.md frontmatter must have effort: xhigh\nActual:\n${fm}`);
|
||||
});
|
||||
|
||||
test('commands/gsd/execute-phase.md has context: fork', () => {
|
||||
test('commands/gsd/execute-phase.md does NOT have context: fork (#921)', () => {
|
||||
const fm = readFrontmatter(path.join(SOURCE_COMMANDS_DIR, 'execute-phase.md'));
|
||||
assert.match(fm, /^context:[ \t]*fork$/m,
|
||||
`execute-phase.md frontmatter must have context: fork\nActual:\n${fm}`);
|
||||
assert.doesNotMatch(fm, /^context:[ \t]*fork$/m,
|
||||
`execute-phase.md is a spawning orchestrator and must NOT have context: fork (#921)\nActual:\n${fm}`);
|
||||
});
|
||||
|
||||
test('commands/gsd/execute-phase.md has effort: xhigh', () => {
|
||||
@@ -115,10 +127,10 @@ describe('#769 source commands: heavy skills have context: fork and effort: xhig
|
||||
`execute-phase.md frontmatter must have effort: xhigh\nActual:\n${fm}`);
|
||||
});
|
||||
|
||||
test('commands/gsd/plan-phase.md has context: fork', () => {
|
||||
test('commands/gsd/plan-phase.md does NOT have context: fork (#921)', () => {
|
||||
const fm = readFrontmatter(path.join(SOURCE_COMMANDS_DIR, 'plan-phase.md'));
|
||||
assert.match(fm, /^context:[ \t]*fork$/m,
|
||||
`plan-phase.md frontmatter must have context: fork\nActual:\n${fm}`);
|
||||
assert.doesNotMatch(fm, /^context:[ \t]*fork$/m,
|
||||
`plan-phase.md is a spawning orchestrator and must NOT have context: fork (#921)\nActual:\n${fm}`);
|
||||
});
|
||||
|
||||
test('commands/gsd/plan-phase.md has effort: xhigh', () => {
|
||||
@@ -237,7 +249,9 @@ describe('#769 convertClaudeCommandToClaudeSkill: preserves context and effort f
|
||||
|
||||
// ─── describe 3: Claude global install — SKILL.md files include new fields ────
|
||||
|
||||
describe('#769 Claude global install: SKILL.md files preserve context: fork and effort:', () => {
|
||||
// #921/#922: after install, spawning orchestrators must NOT carry context: fork
|
||||
// in their emitted SKILL.md. effort: xhigh is still emitted (preserved from source).
|
||||
describe('#769/#921 Claude global install: spawning-orchestrator SKILL.md files have effort: xhigh but NOT context: fork', () => {
|
||||
let tmpDir;
|
||||
let claudeHome;
|
||||
|
||||
@@ -251,12 +265,12 @@ describe('#769 Claude global install: SKILL.md files preserve context: fork and
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
test('gsd-autonomous SKILL.md has context: fork after global install', () => {
|
||||
test('gsd-autonomous SKILL.md does NOT have context: fork after global install (#921)', () => {
|
||||
runClaudeGlobalInstall(claudeHome);
|
||||
const skillPath = path.join(claudeHome, 'skills', 'gsd-autonomous', 'SKILL.md');
|
||||
const fm = readFrontmatter(skillPath);
|
||||
assert.match(fm, /^context:[ \t]*fork$/m,
|
||||
`gsd-autonomous SKILL.md must have context: fork\nActual:\n${fm}`);
|
||||
assert.doesNotMatch(fm, /^context:[ \t]*fork$/m,
|
||||
`gsd-autonomous is a spawning orchestrator; its SKILL.md must NOT have context: fork (#921)\nActual:\n${fm}`);
|
||||
});
|
||||
|
||||
test('gsd-autonomous SKILL.md has effort: xhigh after global install', () => {
|
||||
@@ -267,12 +281,12 @@ describe('#769 Claude global install: SKILL.md files preserve context: fork and
|
||||
`gsd-autonomous SKILL.md must have effort: xhigh\nActual:\n${fm}`);
|
||||
});
|
||||
|
||||
test('gsd-execute-phase SKILL.md has context: fork after global install', () => {
|
||||
test('gsd-execute-phase SKILL.md does NOT have context: fork after global install (#921)', () => {
|
||||
runClaudeGlobalInstall(claudeHome);
|
||||
const skillPath = path.join(claudeHome, 'skills', 'gsd-execute-phase', 'SKILL.md');
|
||||
const fm = readFrontmatter(skillPath);
|
||||
assert.match(fm, /^context:[ \t]*fork$/m,
|
||||
`gsd-execute-phase SKILL.md must have context: fork\nActual:\n${fm}`);
|
||||
assert.doesNotMatch(fm, /^context:[ \t]*fork$/m,
|
||||
`gsd-execute-phase is a spawning orchestrator; its SKILL.md must NOT have context: fork (#921)\nActual:\n${fm}`);
|
||||
});
|
||||
|
||||
test('gsd-execute-phase SKILL.md has effort: xhigh after global install', () => {
|
||||
@@ -283,12 +297,12 @@ describe('#769 Claude global install: SKILL.md files preserve context: fork and
|
||||
`gsd-execute-phase SKILL.md must have effort: xhigh\nActual:\n${fm}`);
|
||||
});
|
||||
|
||||
test('gsd-plan-phase SKILL.md has context: fork after global install', () => {
|
||||
test('gsd-plan-phase SKILL.md does NOT have context: fork after global install (#921)', () => {
|
||||
runClaudeGlobalInstall(claudeHome);
|
||||
const skillPath = path.join(claudeHome, 'skills', 'gsd-plan-phase', 'SKILL.md');
|
||||
const fm = readFrontmatter(skillPath);
|
||||
assert.match(fm, /^context:[ \t]*fork$/m,
|
||||
`gsd-plan-phase SKILL.md must have context: fork\nActual:\n${fm}`);
|
||||
assert.doesNotMatch(fm, /^context:[ \t]*fork$/m,
|
||||
`gsd-plan-phase is a spawning orchestrator; its SKILL.md must NOT have context: fork (#921)\nActual:\n${fm}`);
|
||||
});
|
||||
|
||||
test('gsd-plan-phase SKILL.md has effort: xhigh after global install', () => {
|
||||
|
||||
@@ -193,3 +193,70 @@ describe('plan-phase workflow: top-level spawn guard (#913)', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── (D) Attempt-based Agent gate (#922) ─────────────────────────────────────
|
||||
|
||||
describe('plan-phase workflow: attempt-based Agent availability gate (#922)', () => {
|
||||
// Extract the runtime_compatibility block for targeted assertions
|
||||
const rtBlock = (() => {
|
||||
const m = workflow.match(/<runtime_compatibility>([\s\S]*?)<\/runtime_compatibility>/);
|
||||
return m ? m[1] : '';
|
||||
})();
|
||||
|
||||
// Extract the "Other runtimes" clause specifically
|
||||
const otherRuntimesClause = (() => {
|
||||
const m = rtBlock.match(/\*\*Other runtimes[^*]*\*\*[^\n]*\n([\s\S]*?)(?=\n\*\*|$)/);
|
||||
return m ? m[0] : rtBlock;
|
||||
})();
|
||||
|
||||
test('Other runtimes clause does not authorize stopping on a self-assessed absence (#922)', () => {
|
||||
// The pre-#922 wording ("if the Agent tool is genuinely absent") let the model
|
||||
// self-assess and stop without ever attempting a call. The fixed wording must
|
||||
// not contain phrasing that authorizes that pattern.
|
||||
const forbiddenPatterns = [
|
||||
/if the Agent tool is genuinely absent/i,
|
||||
/if.*Agent.*genuinely absent/i,
|
||||
];
|
||||
for (const pattern of forbiddenPatterns) {
|
||||
assert.ok(
|
||||
!pattern.test(otherRuntimesClause),
|
||||
`plan-phase "Other runtimes" clause must not authorize stopping on a self-assessed Agent absence — ` +
|
||||
`use attempt-based gate instead (#922). Found: ${otherRuntimesClause.trim()}`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('Other runtimes clause pins "Always attempt the actual Agent() call" language (#922)', () => {
|
||||
// Pin the exact contract phrase so a future edit that changes to "try to determine
|
||||
// availability" or "check if Agent is available" does not silently reintroduce introspection.
|
||||
assert.ok(
|
||||
otherRuntimesClause.includes('Always attempt the actual') ||
|
||||
otherRuntimesClause.includes('always attempt the actual'),
|
||||
`plan-phase "Other runtimes" clause must pin "Always attempt the actual Agent() call" (or equivalent) (#922). ` +
|
||||
`Found: ${otherRuntimesClause.trim()}`
|
||||
);
|
||||
});
|
||||
|
||||
test('Other runtimes clause pins "real tool-unavailable error" as the only valid stop signal (#922)', () => {
|
||||
// Must tie the stop to a real returned error, not a self-assessed absence.
|
||||
assert.ok(
|
||||
otherRuntimesClause.includes('real tool-unavailable error') ||
|
||||
otherRuntimesClause.includes('tool-unavailable error returned'),
|
||||
`plan-phase "Other runtimes" clause must state only a real tool-unavailable error from Agent() authorizes stopping (#922). ` +
|
||||
`Found: ${otherRuntimesClause.trim()}`
|
||||
);
|
||||
});
|
||||
|
||||
test('Other runtimes clause still prohibits inline role collapse (#922 preserves #913)', () => {
|
||||
// Even after the attempt-based rewrite the clause must keep the no-inline-collapse guard.
|
||||
const hasNoInline =
|
||||
otherRuntimesClause.toLowerCase().includes('do not') &&
|
||||
(otherRuntimesClause.toLowerCase().includes('inline') ||
|
||||
otherRuntimesClause.toLowerCase().includes('collapse'));
|
||||
assert.ok(
|
||||
hasNoInline,
|
||||
`plan-phase "Other runtimes" clause must still prohibit inline role collapse even with the attempt-based gate (#922). ` +
|
||||
`Found: ${otherRuntimesClause.trim()}`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -82,7 +82,7 @@ const GRACE = 3000;
|
||||
// XL high-water mark is execute-phase.md — note that under LINES it was
|
||||
// plan-phase; bytes genuinely re-rank the tier, which is the point of #717.
|
||||
// actualMax=92525 (execute-phase, #913 inline-fallback scope clarification);
|
||||
// slack=475 ≤ GRACE. plan-phase.md=90501 (#913 runtime_compatibility block + label rename), new-project.md=58110.
|
||||
// slack=475 ≤ GRACE. plan-phase.md=90748 (#922 attempt-based Agent gate), new-project.md=58110.
|
||||
const XL_BUDGET = 93000;
|
||||
// LARGE high-water mark is docs-update.md. actualMax=54410 (#891 launcher shim expansion);
|
||||
// slack=1590 ≤ GRACE. quick.md=45710, autonomous.md=38030.
|
||||
@@ -96,7 +96,7 @@ const DEFAULT_BUDGET = 40000;
|
||||
// pattern that future shrinks should follow. Byte counts noted for reference.
|
||||
const XL_WORKFLOWS = new Set([
|
||||
'execute-phase', // 92525 bytes (tier high-water mark; grew in #913 inline-fallback scope clarification)
|
||||
'plan-phase', // 90501 bytes (grew in #913 runtime_compatibility block + label rename)
|
||||
'plan-phase', // 90748 bytes (grew in #922 attempt-based Agent gate)
|
||||
'new-project', // 55850 bytes
|
||||
]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user