specrails-core 6.3.0 → 6.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-runtime/capabilities.js +22 -5
- package/dist/agent-runtime/cli-executor.js +8 -3
- package/dist/agent-runtime/cli.d.ts +1 -1
- package/dist/agent-runtime/cli.js +50 -3
- package/dist/agent-runtime/compact/environment.d.ts +49 -4
- package/dist/agent-runtime/compact/environment.js +191 -8
- package/dist/agent-runtime/config.js +68 -53
- package/dist/agent-runtime/engine/compiler.js +10 -2
- package/dist/agent-runtime/engine/contracts.d.ts +16 -0
- package/dist/agent-runtime/engine/pieces/control.js +11 -2
- package/dist/agent-runtime/engine/pieces/role-turn.d.ts +5 -0
- package/dist/agent-runtime/engine/pieces/role-turn.js +69 -3
- package/dist/agent-runtime/engine/pieces/verify.d.ts +10 -3
- package/dist/agent-runtime/engine/pieces/verify.js +105 -30
- package/dist/agent-runtime/executor-types.d.ts +2 -0
- package/dist/agent-runtime/graph/nodes.js +9 -55
- package/dist/agent-runtime/prompts.d.ts +1 -1
- package/dist/agent-runtime/prompts.js +18 -12
- package/dist/agent-runtime/session/application/active-session.d.ts +176 -0
- package/dist/agent-runtime/session/application/active-session.js +701 -0
- package/dist/agent-runtime/session/application/delegation.d.ts +73 -0
- package/dist/agent-runtime/session/application/delegation.js +171 -0
- package/dist/agent-runtime/session/application/output-buffer.d.ts +43 -0
- package/dist/agent-runtime/session/application/output-buffer.js +65 -0
- package/dist/agent-runtime/session/application/session-service.d.ts +121 -0
- package/dist/agent-runtime/session/application/session-service.js +256 -0
- package/dist/agent-runtime/session/application/timers.d.ts +15 -0
- package/dist/agent-runtime/session/application/timers.js +35 -0
- package/dist/agent-runtime/session/domain/descriptor.d.ts +6 -0
- package/dist/agent-runtime/session/domain/descriptor.js +32 -0
- package/dist/agent-runtime/session/domain/errors.d.ts +9 -0
- package/dist/agent-runtime/session/domain/errors.js +16 -0
- package/dist/agent-runtime/session/domain/events.d.ts +147 -0
- package/dist/agent-runtime/session/domain/events.js +24 -0
- package/dist/agent-runtime/session/domain/fingerprint.d.ts +2 -0
- package/dist/agent-runtime/session/domain/fingerprint.js +10 -0
- package/dist/agent-runtime/session/domain/interruption.d.ts +13 -0
- package/dist/agent-runtime/session/domain/interruption.js +39 -0
- package/dist/agent-runtime/session/domain/policy.d.ts +42 -0
- package/dist/agent-runtime/session/domain/policy.js +104 -0
- package/dist/agent-runtime/session/domain/snapshot.d.ts +100 -0
- package/dist/agent-runtime/session/domain/snapshot.js +234 -0
- package/dist/agent-runtime/session/domain/state-machines.d.ts +21 -0
- package/dist/agent-runtime/session/domain/state-machines.js +41 -0
- package/dist/agent-runtime/session/domain/types.d.ts +130 -0
- package/dist/agent-runtime/session/domain/types.js +15 -0
- package/dist/agent-runtime/session/domain/usage.d.ts +54 -0
- package/dist/agent-runtime/session/domain/usage.js +86 -0
- package/dist/agent-runtime/session/drivers/claude/argv.d.ts +19 -0
- package/dist/agent-runtime/session/drivers/claude/argv.js +67 -0
- package/dist/agent-runtime/session/drivers/claude/driver.d.ts +19 -0
- package/dist/agent-runtime/session/drivers/claude/driver.js +134 -0
- package/dist/agent-runtime/session/drivers/claude/translator.d.ts +40 -0
- package/dist/agent-runtime/session/drivers/claude/translator.js +247 -0
- package/dist/agent-runtime/session/drivers/codex/argv.d.ts +12 -0
- package/dist/agent-runtime/session/drivers/codex/argv.js +76 -0
- package/dist/agent-runtime/session/drivers/codex/driver.d.ts +19 -0
- package/dist/agent-runtime/session/drivers/codex/driver.js +193 -0
- package/dist/agent-runtime/session/drivers/codex/rpc.d.ts +28 -0
- package/dist/agent-runtime/session/drivers/codex/rpc.js +81 -0
- package/dist/agent-runtime/session/drivers/codex/translator.d.ts +53 -0
- package/dist/agent-runtime/session/drivers/codex/translator.js +267 -0
- package/dist/agent-runtime/session/drivers/common/process.d.ts +37 -0
- package/dist/agent-runtime/session/drivers/common/process.js +116 -0
- package/dist/agent-runtime/session/drivers/executor/driver.d.ts +22 -0
- package/dist/agent-runtime/session/drivers/executor/driver.js +163 -0
- package/dist/agent-runtime/session/drivers/registry.d.ts +32 -0
- package/dist/agent-runtime/session/drivers/registry.js +38 -0
- package/dist/agent-runtime/session/host/protocol.d.ts +18 -0
- package/dist/agent-runtime/session/host/protocol.js +88 -0
- package/dist/agent-runtime/session/host/server.d.ts +57 -0
- package/dist/agent-runtime/session/host/server.js +229 -0
- package/dist/agent-runtime/session/index.d.ts +45 -0
- package/dist/agent-runtime/session/index.js +74 -0
- package/dist/agent-runtime/session/journal/schema.d.ts +7 -0
- package/dist/agent-runtime/session/journal/schema.js +44 -0
- package/dist/agent-runtime/session/journal/sqlite-journal.d.ts +70 -0
- package/dist/agent-runtime/session/journal/sqlite-journal.js +279 -0
- package/dist/agent-runtime/session/ports.d.ts +192 -0
- package/dist/agent-runtime/session/ports.js +1 -0
- package/dist/agent-runtime/session/testing/driver-conformance.d.ts +21 -0
- package/dist/agent-runtime/session/testing/driver-conformance.js +97 -0
- package/dist/agent-runtime/session/testing/fake-clock.d.ts +20 -0
- package/dist/agent-runtime/session/testing/fake-clock.js +34 -0
- package/dist/agent-runtime/session/testing/fixture-replayer.d.ts +41 -0
- package/dist/agent-runtime/session/testing/fixture-replayer.js +172 -0
- package/dist/agent-runtime/session/testing/index.d.ts +9 -0
- package/dist/agent-runtime/session/testing/index.js +9 -0
- package/dist/agent-runtime/session/testing/memory-journal.d.ts +27 -0
- package/dist/agent-runtime/session/testing/memory-journal.js +70 -0
- package/dist/agent-runtime/session/testing/scripted-driver.d.ts +30 -0
- package/dist/agent-runtime/session/testing/scripted-driver.js +77 -0
- package/dist/agent-runtime/verification-proposals.d.ts +9 -0
- package/dist/agent-runtime/verification-proposals.js +69 -0
- package/dist/agent-runtime/verification-repair.d.ts +26 -0
- package/dist/agent-runtime/verification-repair.js +88 -0
- package/dist/installer/util/registry.js +3 -3
- package/dist/shared/specrails-home.d.ts +21 -0
- package/dist/shared/specrails-home.js +36 -0
- package/integration-contract.json +43 -2
- package/package.json +11 -1
- package/schemas/agent-runtime.schema.json +9 -7
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { guardrailEnabled } from '../guardrails.js';
|
|
2
2
|
import { verificationFailureSummary } from '../verification-diagnostics.js';
|
|
3
3
|
import { addDeveloperChecks, bindPlan, expandedPlanCommands, initializeVerificationPlan, readVerificationPlan, validateProposedChecks } from '../verification-plan.js';
|
|
4
|
-
import { existsSync } from 'node:fs';
|
|
5
4
|
import path from 'node:path';
|
|
6
5
|
import { OpenSpecTools } from '../openspec.js';
|
|
7
6
|
import { candidateManifest, fingerprintCandidate, frozenAcceptanceCriteria, inspectPipeline, recordAcceptance, transitionPipeline, validateAcceptanceReport, verifyPipeline, } from '../../pipeline/pipeline-state.js';
|
|
8
7
|
import { ARCHITECT_OUTPUT_SCHEMA, DEVELOPER_OUTPUT_SCHEMA, REVIEW_OUTPUT_SCHEMA, correctionInstructions, deepenInstructions, roleInstructions } from '../prompts.js';
|
|
9
8
|
import { archive, child, journal, object, parseArchitecture, proposedVerification, write, writeDesignConfidence } from './artifacts.js';
|
|
10
9
|
import { evaluateReview } from './review-policy.js';
|
|
11
|
-
import {
|
|
10
|
+
import { isEnvironmentFailure } from '../compact/environment.js';
|
|
11
|
+
import { hostPreconditionMessage, preconditionBlock, repairEnvironment } from '../verification-repair.js';
|
|
12
12
|
import { unreachedTestFiles, unreachedTestsReason } from '../compact/test-reachability.js';
|
|
13
13
|
import { exitCodeContradiction, exitHonestyReason } from '../compact/exit-code-honesty.js';
|
|
14
14
|
import { boundedReviewManifest, reviewChanges } from '../review-context.js';
|
|
@@ -287,52 +287,6 @@ function fixerNode(deps) {
|
|
|
287
287
|
},
|
|
288
288
|
};
|
|
289
289
|
}
|
|
290
|
-
/** Where a missing dependency install belongs: the nearest directory with a lockfile or manifest above the failing command, else the repository root. */
|
|
291
|
-
const INSTALL_MARKERS = ['package-lock.json', 'npm-shrinkwrap.json', 'yarn.lock', 'pnpm-lock.yaml', 'bun.lock', 'bun.lockb', 'requirements.txt', 'pyproject.toml', 'go.mod', 'Cargo.toml'];
|
|
292
|
-
function installRoots(context, commands) {
|
|
293
|
-
const roots = [];
|
|
294
|
-
for (const command of commands) {
|
|
295
|
-
const repository = context.repositories.find(repo => repo.id === command.repositoryId);
|
|
296
|
-
if (!repository)
|
|
297
|
-
continue;
|
|
298
|
-
let directory = command.cwd;
|
|
299
|
-
let chosen = repository.path;
|
|
300
|
-
for (;;) {
|
|
301
|
-
const relative = path.relative(repository.path, directory);
|
|
302
|
-
if (relative.startsWith('..') || path.isAbsolute(relative))
|
|
303
|
-
break;
|
|
304
|
-
if (INSTALL_MARKERS.some(name => existsSync(path.join(directory, name)))) {
|
|
305
|
-
chosen = directory;
|
|
306
|
-
break;
|
|
307
|
-
}
|
|
308
|
-
if (!relative)
|
|
309
|
-
break;
|
|
310
|
-
directory = path.dirname(directory);
|
|
311
|
-
}
|
|
312
|
-
roots.push(chosen);
|
|
313
|
-
}
|
|
314
|
-
// Every repository root stays a candidate: its plan is a no-op when it is already installed.
|
|
315
|
-
return [...new Set([...roots, ...context.repositories.map(repository => repository.path)])];
|
|
316
|
-
}
|
|
317
|
-
/** A directory as the log should name it: relative to the checkout that contains it. */
|
|
318
|
-
function checkoutRelative(context, directory) {
|
|
319
|
-
const repository = context.repositories.find(repo => { const relative = path.relative(repo.path, directory); return !relative.startsWith('..') && !path.isAbsolute(relative); });
|
|
320
|
-
return repository ? path.relative(repository.path, directory).split(path.sep).join('/') || '.' : directory;
|
|
321
|
-
}
|
|
322
|
-
function hostPreconditionMessage(reason, command, args, directory) {
|
|
323
|
-
return `Verification cannot run in this environment: ${reason} (\`${[command, ...args].join(' ')}\` in ${directory}). This is not a defect in the change, so no correction round was started. Make it available to Specrails (for example in the login shell profile Specrails loads, or with a refreshed registry token), then resume.`;
|
|
324
|
-
}
|
|
325
|
-
/** A failed check the host must unblock (credentials, registry access, a missing variable): no correction round can repair it. */
|
|
326
|
-
function preconditionBlock(context, receipt) {
|
|
327
|
-
for (const command of receipt.commands) {
|
|
328
|
-
if (command.exitCode === 0)
|
|
329
|
-
continue;
|
|
330
|
-
const reason = hostPreconditionFailure(command.output);
|
|
331
|
-
if (reason)
|
|
332
|
-
return hostPreconditionMessage(reason, command.command, command.args, checkoutRelative(context, command.cwd));
|
|
333
|
-
}
|
|
334
|
-
return undefined;
|
|
335
|
-
}
|
|
336
290
|
function verifyNode(deps) {
|
|
337
291
|
const { context, note, config } = deps;
|
|
338
292
|
const roots = context.repositories.map(repository => repository.path);
|
|
@@ -388,25 +342,25 @@ function verifyNode(deps) {
|
|
|
388
342
|
note('fixer', 'Verification failed on the environment, not on the change; stopping for the host instead of starting a correction round.');
|
|
389
343
|
return { status: 'blocked', error: reason, usage: NO_SPEND };
|
|
390
344
|
};
|
|
345
|
+
const blockerMessage = (blocker) => hostPreconditionMessage(blocker.reason, blocker.command, blocker.args, blocker.cwd);
|
|
391
346
|
let precondition = receipt.valid ? undefined : preconditionBlock(context, receipt);
|
|
392
347
|
if (precondition)
|
|
393
|
-
return hostBlocked(precondition);
|
|
348
|
+
return hostBlocked(blockerMessage(precondition));
|
|
394
349
|
// A failure that is really a missing toolchain or dependency (exit 127,
|
|
395
350
|
// "command not found", "Cannot find module"…) is the HOST's to fix, not
|
|
396
351
|
// the model's — for every developer: a fresh worktree has no
|
|
397
352
|
// node_modules whatever wrote the code. Install once where the failing
|
|
398
353
|
// command runs and re-verify; only a second failure becomes feedback.
|
|
399
354
|
if (!receipt.valid && guardrailEnabled(config.guardrails, 'environment-repair') && receipt.commands.some(command => isEnvironmentFailure(command.exitCode, command.output))) {
|
|
400
|
-
const
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
if (installs.some(outcome => outcome.ok)) {
|
|
355
|
+
const repair = repairEnvironment(context, receipt, config.guardrails, text => note('developer', text));
|
|
356
|
+
if (repair.refused)
|
|
357
|
+
return hostBlocked(blockerMessage(repair.refused));
|
|
358
|
+
if (repair.installs.some(outcome => outcome.ok)) {
|
|
405
359
|
note('developer', 'Verification failed on the environment (missing dependencies or tools); the host installed them and is verifying again.');
|
|
406
360
|
receipt = await run();
|
|
407
361
|
precondition = receipt.valid ? undefined : preconditionBlock(context, receipt);
|
|
408
362
|
if (precondition)
|
|
409
|
-
return hostBlocked(precondition);
|
|
363
|
+
return hostBlocked(blockerMessage(precondition));
|
|
410
364
|
}
|
|
411
365
|
}
|
|
412
366
|
const evidence = {
|
|
@@ -3,7 +3,7 @@ import type { PipelineContext, VerificationCommand } from '../pipeline/pipeline-
|
|
|
3
3
|
import { type ReviewPolicy } from './graph/review-policy.js';
|
|
4
4
|
import type { DeveloperRecord } from './graph/state.js';
|
|
5
5
|
/** Bump whenever the wording changes: the version is part of the frozen run identity. */
|
|
6
|
-
export declare const ROLE_INSTRUCTIONS_VERSION = "
|
|
6
|
+
export declare const ROLE_INSTRUCTIONS_VERSION = "14";
|
|
7
7
|
export interface RoleFeedback {
|
|
8
8
|
verification?: unknown;
|
|
9
9
|
review?: unknown;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { isBuiltinRole } from './executor-types.js';
|
|
2
2
|
import { DEFAULT_REVIEW_POLICY, REVIEW_ASPECTS } from './graph/review-policy.js';
|
|
3
3
|
/** Bump whenever the wording changes: the version is part of the frozen run identity. */
|
|
4
|
-
export const ROLE_INSTRUCTIONS_VERSION = '
|
|
4
|
+
export const ROLE_INSTRUCTIONS_VERSION = '14';
|
|
5
5
|
const OUTPUT_TAIL = 6_000;
|
|
6
6
|
/** One senior engineer, rendered once per role prompt (never repeated inside it): the identity the implement pipeline's roles share. */
|
|
7
7
|
const ENGINEER = 'a T-shaped principal engineer with decades of delivery across web, mobile and backend systems: deep in software design and testing, broad across product, UX, data, security, infrastructure and operations; fluent in hexagonal (ports and adapters) architecture, SOLID, design patterns, Clean Code, The Pragmatic Programmer, refactoring, legacy-code seams and AI-assisted development. You work for the user, the operator and the next maintainer, and you treat AI-generated code, including your own, with the scepticism owed to any untrusted contribution.';
|
|
@@ -116,7 +116,7 @@ function boundarySection(role, custom = false) {
|
|
|
116
116
|
role === 'developer'
|
|
117
117
|
? [
|
|
118
118
|
'- Edit only the repositories in scope, and only inside a repository\'s scope when it declares one. Prefer the smallest change that fully satisfies the tasks; do not refactor, reformat, rename or upgrade unrelated code, tests or configuration. On a fixer turn, a failing host test in that scope may receive the minimal compatibility repair described in the correction instructions; preserve its required behavior and assertions. Other unrelated problems belong in your summary.',
|
|
119
|
-
'- Never edit package-manager, registry, credential, CI or environment configuration (for example `.npmrc`, `.yarnrc*`, `.env*`, CI workflows) to make a command work. A missing credential, environment variable, registry access or tool is the host\'s to fix: report it under `incomplete` with the exact error.',
|
|
119
|
+
'- Never edit package-manager, registry, credential, CI or environment configuration (for example `.npmrc`, `.yarnrc*`, `.env*`, CI workflows) to make a command work. A missing credential, environment variable, registry access or tool is the host\'s to fix: report it under `incomplete` with the exact error. Installing a documented, idempotent toolchain artifact inside the admitted workspace (for example a Playwright browser with `npx playwright install <browser>`, or a Python virtualenv) is allowed and must be reported under verification; editing package-manager, registry, credential, CI or environment configuration remains forbidden.',
|
|
120
120
|
'- Check with the narrowest command that covers what you touched (the specific test files or test names), never the whole checkout or every workspace; Core runs the complete verification plan after your turn.',
|
|
121
121
|
].join('\n')
|
|
122
122
|
: role === 'architect' ? '- Read code without modifying it. Author OpenSpec artifacts only through the supplied scoped workflow tools.' + (custom ? '' : ' Return confidence and verification metadata in JSON.') : '- This role is read-only. Do not create, edit or delete files' + (custom ? '.' : '; return your result as the requested JSON object.'),
|
|
@@ -191,7 +191,7 @@ function fixerSection(verification, definition) {
|
|
|
191
191
|
'4. Fix the cause, not the symptom: a repair that satisfies the assertion while leaving the defect, or that special-cases the test input, is not a fix. Fix incorrect implementation behavior. A failing mandatory host test inside the admitted repository/workspace may also receive a minimal compatibility repair when you prove that its expected behavior is unchanged. For example, a source assertion broken only by formatting may tolerate whitespace while retaining every required operand and guard. This applies even to a pre-existing test outside the current changed-file list. Explain the original expectation and why the repair still rejects the prohibited behavior. Never weaken an assertion, delete or skip a test, hardcode success, change acceptance criteria, add dependencies or widen the repair beyond the diagnosed failure.',
|
|
192
192
|
'5. A test file the host reports as never executed must be wired into the repository\'s test command (the test script or runner configuration) and then made to pass.',
|
|
193
193
|
'6. Confirm the repair with the focused test command and a negative case for any repaired assertion: removing a required safety guard or returning the wrong value must still fail. Preserve the original test exit status; never pipe test execution through grep/head or another command that masks it. Capture stdout/stderr in a temporary log and inspect that log separately. Core runs the complete verification plan after your turn.',
|
|
194
|
-
'7. Inspect only the reported files, direct dependencies and the evidence needed to establish the cause. Never edit outside the admitted repository/workspace, credentials, environment or unrelated configuration.
|
|
194
|
+
'7. Inspect only the reported files, direct dependencies and the evidence needed to establish the cause. Never edit outside the admitted repository/workspace, credentials, environment or unrelated configuration. When the diagnosed cause is a host precondition or environment blocker (missing network, credentials, environment variable, toolchain, setup command or an out-of-scope repository), make no speculative edits, state in summary that the candidate was intentionally left unchanged, and return `blocker` as {"kind":"network|credential|environment-variable|toolchain|setup|environment|scope","command":"…","cwd":"…","evidence":"exact error","requiredAction":"one imperative sentence the host can act on"}. `proposal.md`, `design.md` and the specs remain frozen; only tick completed tasks.',
|
|
195
195
|
'8. Return the requested JSON contract. Include the diagnosis, exact focused commands and original exit codes, repair and preserved assertion in summary. List every unresolved failure and its reason under incomplete, even when the implementation tasks are already ticked. A failed test remains failed until real host verification passes.',
|
|
196
196
|
] : [definition, ''];
|
|
197
197
|
return [...lines, ...developerTail(verification)];
|
|
@@ -208,7 +208,7 @@ function developerSection(verification, corrections, definition) {
|
|
|
208
208
|
'1. Load and execute openspec-apply-change through the supplied binding. Consult its status and instructions apply, read the context files OpenSpec returns, then the relevant existing code and tests.',
|
|
209
209
|
'2. Work task by task in order. Use test-driven development: write or extend the test first, make it pass with the smallest correct change, then tidy up. Run only focused tests that cover what you touched while iterating. Core owns the complete verification plan and runs it after your turn; do not duplicate that full run. Fix the precise failures Core returns on a correction pass.',
|
|
210
210
|
'3. Immediately after completing each task, mark it `- [x]` in `tasks.md`; do not postpone all progress updates until the end of the phase. Only mark tasks whose code and tests are complete. Change nothing else in `tasks.md`, and never edit `proposal.md`, `design.md` or the specs: those documents are frozen, and editing them invalidates the run. If a task cannot be completed, leave it `- [ ]` and list it under `incomplete` with the reason.',
|
|
211
|
-
'4. Keep the implementation consistent with the repository: naming, error handling, import style, formatting and existing utilities. Do not add dependencies unless the design requires them. Change only what the tasks require: the diff should contain the requested change and its tests, nothing else.',
|
|
211
|
+
'4. Keep the implementation consistent with the repository: naming, error handling, import style, formatting and existing utilities. Do not add dependencies unless the design requires them. Change only what the tasks require: the diff should contain the requested change and its tests, nothing else. Never validate with a temporary configuration, alternate runner or local browser the host verification plan does not use, and never delete such a file to hide it: the host runs the plan as-is. When a required tool is missing, install it through the project\'s documented command or report it as a blocker.',
|
|
212
212
|
'Blast radius: the files design.md and tasks.md name are your boundary. Touch a file outside it only when a task cannot be completed otherwise; keep that edit to the strict need and explain it in summary. Inside a file, change only the lines the task needs: no reformatting, import reordering, renames, type widening, comment rewrites or "while I am here" fixes. Leave an unrelated problem alone and mention it in summary. Keep public signatures, exported contracts, schemas and persisted formats as the design states. Never delete or rewrite a test a task does not name, and never regenerate lockfiles, snapshots or generated files unless the task\'s own change requires it.',
|
|
213
213
|
'Quality bar: write code a senior maintainer would merge unchanged. Intention-revealing names from the domain vocabulary; small functions that do one thing at one level of abstraction; guard clauses over nested conditionals; no boolean flag parameters, magic values or hidden side effects; immutability by default and explicit types at module boundaries; parse and validate at the boundary, trust typed values inside; every error path handled the way neighbouring code handles it (fail fast at the boundary, never swallow, errors carry context); domain logic out of adapters and dependencies pointing inward as the repository already does; comments explain why, never what; no dead code, commented-out code, debug output, TODO placeholders or speculative options.',
|
|
214
214
|
'Engineering judgement: understand before you change and never program by coincidence (if you cannot explain why it works, you are not done); never delete or bypass a guard, branch or workaround you cannot explain. Handle what tests rarely reach: empty, huge and malformed inputs, boundary values, time zones and Unicode, partial failures, retries with idempotency, races, cancellation, timeouts and resource cleanup (handles, listeners, subscriptions, temp files). Security hygiene is non-negotiable: parameterized queries, escaped output, no secrets or personal data in code or logs, least privilege, authorization where the repository enforces it. Logs and metrics follow the repository\'s conventions; migrations and persisted-format changes stay additive and backward compatible. Verify every API, signature and option against the source or installed types, never from memory. Do the simplest thing that fully works, then refactor only inside the blast radius. Tests are the specification: one behavior per test (arrange, act, assert) covering inputs, outputs, side effects and errors; no logic in tests; mock only at real boundaries; deterministic; failing without the change; in the repository\'s existing style. A task is done only when its code, tests and every artifact it makes stale (localized strings in every shipped locale, documentation, schemas, configuration examples) are updated and its focused checks pass.',
|
|
@@ -220,14 +220,18 @@ function developerSection(verification, corrections, definition) {
|
|
|
220
220
|
return [...lines, ...developerTail(verification)];
|
|
221
221
|
}
|
|
222
222
|
/** Verification commands, durable-progress guidance and the output contract shared by the developer and fixer stances. */
|
|
223
|
+
/** The complete host verification plan a write role must see: what Core runs after the turn, so nothing is validated through a bypass the plan does not use. */
|
|
224
|
+
function hostPlanSection(verification) {
|
|
225
|
+
if (!verification?.length)
|
|
226
|
+
return [];
|
|
227
|
+
return [
|
|
228
|
+
'', 'Core owns these complete verification commands and will run them after your turn. Use focused tests while iterating instead of repeating this plan:',
|
|
229
|
+
...verification.map(command => `- repository \`${command.repositoryId}\`${command.cwd ? ' in `' + command.cwd + '`' : ''}: \`${shellWords(command)}\``),
|
|
230
|
+
];
|
|
231
|
+
}
|
|
223
232
|
function developerTail(verification) {
|
|
224
|
-
const lines = [];
|
|
225
|
-
|
|
226
|
-
lines.push('', 'Core owns these complete verification commands and will run them after your turn. Use focused tests while iterating instead of repeating this plan:');
|
|
227
|
-
for (const command of verification)
|
|
228
|
-
lines.push(`- repository \`${command.repositoryId}\`${command.cwd ? ' in `' + command.cwd + '`' : ''}: \`${shellWords(command)}\``);
|
|
229
|
-
}
|
|
230
|
-
lines.push('', '## Durable implementation progress', '', 'When loading the official apply skill, read its savedProgress handoff before repeating investigation. Reconcile it with the current diff and tasks; it is advisory history, never accepted verification evidence. Preserve completed work and previously discovered test commands or environment blockers. Recheck a blocker only when its relevant condition changed.', 'After each completed task, and whenever the next action or a blocker changes, call the scoped workflow action write_progress with progress: {summary, completedTasks, nextTasks, checks: [{command, outcome}], blockers}. Replace the previous handoff with a concise current account (at most 8 KB); include exact focused test commands, original exit outcomes, relevant paths and the next discriminating action. Keep nextTasks concrete enough for a fresh session to continue unfinished work. Do not include full logs, credentials or claims that old checks authorize acceptance. The host persists this record outside the frozen OpenSpec artifacts. Use read_progress to refresh it. If interrupted, the next session receives it from load_skill; do not write runtime files directly.', '', '## Output contract', '', 'Finish with exactly one JSON object and nothing after it: no prose after the object, no Markdown fence.', '', '```', '{"summary":"What you implemented and how","files":["src/feature.ts"],"tests":["src/feature.test.ts"],"verification":"npm test passed (12 tests)","incomplete":[{"task":"3. …","reason":"why it could not be completed"}]}', '```', '', '- `files` and `tests`: repository-relative paths you created or modified (test files appear in `tests`, other files in `files`).', '- `verification`: the commands you ran and their outcome, or `none` when no shell was available.', '- `incomplete`: every task still `- [ ]` in `tasks.md`, with its reason; an empty array when everything is done.', '- Do not paste full test logs; the reviewer reads the real verification evidence separately.', '');
|
|
233
|
+
const lines = [...hostPlanSection(verification)];
|
|
234
|
+
lines.push('', '## Durable implementation progress', '', 'When loading the official apply skill, read its savedProgress handoff before repeating investigation. Reconcile it with the current diff and tasks; it is advisory history, never accepted verification evidence. Preserve completed work and previously discovered test commands or environment blockers. Recheck a blocker only when its relevant condition changed.', 'After each completed task, and whenever the next action or a blocker changes, call the scoped workflow action write_progress with progress: {summary, completedTasks, nextTasks, checks: [{command, outcome}], blockers}. Replace the previous handoff with a concise current account (at most 8 KB); include exact focused test commands, original exit outcomes, relevant paths and the next discriminating action. Keep nextTasks concrete enough for a fresh session to continue unfinished work. Do not include full logs, credentials or claims that old checks authorize acceptance. The host persists this record outside the frozen OpenSpec artifacts. Use read_progress to refresh it. If interrupted, the next session receives it from load_skill; do not write runtime files directly.', '', '## Output contract', '', 'Finish with exactly one JSON object and nothing after it: no prose after the object, no Markdown fence.', '', '```', '{"summary":"What you implemented and how","files":["src/feature.ts"],"tests":["src/feature.test.ts"],"verification":"npm test passed (12 tests)","incomplete":[{"task":"3. …","reason":"why it could not be completed"}],"blocker":{"kind":"toolchain","command":"npx playwright test","cwd":".","evidence":"exact error","requiredAction":"Run npx playwright install chromium"}}', '```', '', '- `files` and `tests`: repository-relative paths you created or modified (test files appear in `tests`, other files in `files`).', '- `verification`: the commands you ran and their outcome, or `none` when no shell was available.', '- `incomplete`: every task still `- [ ]` in `tasks.md`, with its reason; an empty array when everything is done.', '- `blocker`: only when the cause lies outside the change; omit otherwise. The host ends the run with your `requiredAction` instead of starting another correction round.', '- Do not paste full test logs; the reviewer reads the real verification evidence separately.', '');
|
|
231
235
|
return lines;
|
|
232
236
|
}
|
|
233
237
|
function reviewerSection(policy, criteria, definition) {
|
|
@@ -388,7 +392,9 @@ export function roleInstructions(roleOrDescriptor, context, change, options = {}
|
|
|
388
392
|
`Workspace access: ${roleOrDescriptor.access}. OpenSpec artifact permission: ${roleOrDescriptor.artifacts}.`, '',
|
|
389
393
|
'Execute only this assigned role. Traversal, retries, verification, approval, archive and delivery belong to Core. Do not spawn another role or change runtime metadata.',
|
|
390
394
|
...boundarySection(roleOrDescriptor.access === 'write' ? 'developer' : roleOrDescriptor.artifacts === 'all' ? 'architect' : 'reviewer', true),
|
|
391
|
-
...conventionsSection(), ...scopeSection(context, change),
|
|
395
|
+
...conventionsSection(), ...scopeSection(context, change),
|
|
396
|
+
...(roleOrDescriptor.access === 'write' ? hostPlanSection(options.verification) : []),
|
|
397
|
+
...feedbackSection(options.feedback),
|
|
392
398
|
].join('\n').trimEnd() + '\n';
|
|
393
399
|
}
|
|
394
400
|
const feedback = feedbackSection(options.feedback);
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import type { SessionEventEnvelope, ToolActivity } from '../domain/events.js';
|
|
2
|
+
import { type SessionSnapshot } from '../domain/snapshot.js';
|
|
3
|
+
import { type SessionPolicy, type SubagentPhase, type Usage } from '../domain/types.js';
|
|
4
|
+
import { type RateCard } from '../domain/usage.js';
|
|
5
|
+
import type { Clock, CloseReason, DriverFactory, DriverInput, Ids, SessionJournal } from '../ports.js';
|
|
6
|
+
import { type OutputLimits } from './output-buffer.js';
|
|
7
|
+
export interface ActiveSessionDeps {
|
|
8
|
+
journal: SessionJournal;
|
|
9
|
+
factory: DriverFactory;
|
|
10
|
+
clock: Clock;
|
|
11
|
+
ids: Ids;
|
|
12
|
+
rateCard: (driver: string, model: string) => RateCard | null;
|
|
13
|
+
publish: (envelopes: SessionEventEnvelope[]) => void;
|
|
14
|
+
/** Called whenever the session's process liveness or recency changes (LRU bookkeeping). */
|
|
15
|
+
touched: (session: ActiveSession) => void;
|
|
16
|
+
outputLimits?: OutputLimits;
|
|
17
|
+
/** How long `interrupt` waits for the provider before ending the process. */
|
|
18
|
+
interruptGraceMs?: number;
|
|
19
|
+
/** Delay before buffered deltas are flushed. */
|
|
20
|
+
flushMs?: number;
|
|
21
|
+
}
|
|
22
|
+
export interface SessionConfig {
|
|
23
|
+
cwd: string;
|
|
24
|
+
model: string;
|
|
25
|
+
effort: string | null;
|
|
26
|
+
systemPrompt: string | null;
|
|
27
|
+
policy: SessionPolicy;
|
|
28
|
+
}
|
|
29
|
+
/** How work that was running ends when the process goes away. */
|
|
30
|
+
interface Teardown {
|
|
31
|
+
turnStatus: 'stopped' | 'interrupted' | 'failed';
|
|
32
|
+
subagentPhase: SubagentPhase;
|
|
33
|
+
reason: string;
|
|
34
|
+
error?: string;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Coordinates one session: turns driver facts into journaled events, owns the
|
|
38
|
+
* provider process lifecycle, phases, settlement, limits and teardown.
|
|
39
|
+
* All mutation goes through `commit`, which folds before appending, so the
|
|
40
|
+
* journal never holds an event the domain would reject.
|
|
41
|
+
*/
|
|
42
|
+
export declare class ActiveSession {
|
|
43
|
+
readonly sessionId: string;
|
|
44
|
+
private config;
|
|
45
|
+
private readonly deps;
|
|
46
|
+
state: SessionSnapshot;
|
|
47
|
+
private driver;
|
|
48
|
+
private opening;
|
|
49
|
+
private generation;
|
|
50
|
+
/** Generation whose events are accepted (the live or opening process); null when none. */
|
|
51
|
+
private liveGeneration;
|
|
52
|
+
private closing;
|
|
53
|
+
private baseline;
|
|
54
|
+
private readonly timers;
|
|
55
|
+
private readonly output;
|
|
56
|
+
private readonly descriptor;
|
|
57
|
+
/** Inputs held back for drivers without a native input queue. */
|
|
58
|
+
private readonly heldInputs;
|
|
59
|
+
/** Inputs the application wrote on its own (policy-driven turns). */
|
|
60
|
+
private readonly systemInputIds;
|
|
61
|
+
private pendingDeferred;
|
|
62
|
+
private roster;
|
|
63
|
+
private handoffs;
|
|
64
|
+
private finishedSinceLastTurn;
|
|
65
|
+
/** Finished delegated sub-agents whose results the agent has not received yet. */
|
|
66
|
+
private readonly delegatedResults;
|
|
67
|
+
private lastUserTurnAt;
|
|
68
|
+
lastUsedAt: number;
|
|
69
|
+
constructor(sessionId: string, snapshot: SessionSnapshot, config: SessionConfig, deps: ActiveSessionDeps);
|
|
70
|
+
get processAlive(): boolean;
|
|
71
|
+
get isBusy(): boolean;
|
|
72
|
+
get policy(): SessionPolicy;
|
|
73
|
+
send(input: {
|
|
74
|
+
inputId: string;
|
|
75
|
+
text: string;
|
|
76
|
+
attachments?: DriverInput['attachments'];
|
|
77
|
+
delivery: 'queue' | 'steer';
|
|
78
|
+
}): Promise<{
|
|
79
|
+
inputId: string;
|
|
80
|
+
state: string;
|
|
81
|
+
}>;
|
|
82
|
+
interrupt(): Promise<string | null>;
|
|
83
|
+
stopSubagents(ids?: string[]): Promise<string[]>;
|
|
84
|
+
/**
|
|
85
|
+
* Policy says no sub-agents, yet the provider started one (a provider switch
|
|
86
|
+
* can be ignored by a model or CLI version). Stop it and say so: the policy
|
|
87
|
+
* holds whatever the provider's own switch does.
|
|
88
|
+
*/
|
|
89
|
+
private blockSubagent;
|
|
90
|
+
/** Apply now when nothing is running; otherwise defer to the next idle point. */
|
|
91
|
+
update(changes: {
|
|
92
|
+
model?: string;
|
|
93
|
+
effort?: string;
|
|
94
|
+
systemPrompt?: string;
|
|
95
|
+
policy?: SessionPolicy;
|
|
96
|
+
}): Promise<'applied' | 'deferred'>;
|
|
97
|
+
close(reason: string): Promise<void>;
|
|
98
|
+
/**
|
|
99
|
+
* Record work a previous host left running as interrupted. Nothing is
|
|
100
|
+
* restarted: the next user input carries the interruption notice instead.
|
|
101
|
+
*/
|
|
102
|
+
recoverAfterHostLoss(reason: 'restart' | 'host_lost'): void;
|
|
103
|
+
/** Retire the provider process; running work ends as described by `teardown`. */
|
|
104
|
+
retire(reason: CloseReason, teardown: Teardown): Promise<void>;
|
|
105
|
+
private deliver;
|
|
106
|
+
private ensureDriver;
|
|
107
|
+
private onDriverEvent;
|
|
108
|
+
private handleDriverEvent;
|
|
109
|
+
private startTurn;
|
|
110
|
+
private completeTurn;
|
|
111
|
+
/**
|
|
112
|
+
* Inputs whose receipts never reported completion are completed with their
|
|
113
|
+
* turn: the ones that opened it and the ones steered into it.
|
|
114
|
+
*/
|
|
115
|
+
private completeTurnInputs;
|
|
116
|
+
private settlePhaseAfterTurn;
|
|
117
|
+
private afterSubagentChange;
|
|
118
|
+
private onSettled;
|
|
119
|
+
/** Live = running in our tree and, when the provider reports a roster, still on it. */
|
|
120
|
+
private liveCount;
|
|
121
|
+
private maybeResumeAgent;
|
|
122
|
+
/** Give the agent the results of delegated sub-agents it has not seen. */
|
|
123
|
+
private maybeDeliverDelegatedResults;
|
|
124
|
+
/** Running sub-agents Core launched for this session. */
|
|
125
|
+
delegatedLive(): string[];
|
|
126
|
+
delegatedStarted(child: {
|
|
127
|
+
subagentId: string;
|
|
128
|
+
description: string;
|
|
129
|
+
agentType: string;
|
|
130
|
+
prompt: string;
|
|
131
|
+
driver: string;
|
|
132
|
+
model: string;
|
|
133
|
+
}): void;
|
|
134
|
+
delegatedOutput(subagentId: string, event: {
|
|
135
|
+
channel: 'text';
|
|
136
|
+
delta: string;
|
|
137
|
+
} | {
|
|
138
|
+
channel: 'tool';
|
|
139
|
+
tool: ToolActivity;
|
|
140
|
+
}): void;
|
|
141
|
+
/**
|
|
142
|
+
* A delegated child ended. Its usage is its own provider's spend (billed
|
|
143
|
+
* separately); unless a host already collected the result, the agent gets it
|
|
144
|
+
* when the session next settles.
|
|
145
|
+
*/
|
|
146
|
+
delegatedFinished(subagentId: string, outcome: {
|
|
147
|
+
status: 'completed' | 'failed' | 'stopped' | 'interrupted';
|
|
148
|
+
text: string;
|
|
149
|
+
usage: Usage | null;
|
|
150
|
+
reason?: string;
|
|
151
|
+
collected: boolean;
|
|
152
|
+
}): void;
|
|
153
|
+
/** A host received these results through a wait; never inject them again. */
|
|
154
|
+
collectDelegated(subagentIds: readonly string[]): void;
|
|
155
|
+
private enterIdle;
|
|
156
|
+
private enterBackground;
|
|
157
|
+
private idleTeardown;
|
|
158
|
+
private armTurnInactivity;
|
|
159
|
+
private markActivity;
|
|
160
|
+
private applyChanges;
|
|
161
|
+
private releaseHeldInput;
|
|
162
|
+
/** End whatever was running; used by retire and unexpected exits. */
|
|
163
|
+
private endRunningWork;
|
|
164
|
+
private turnUsage;
|
|
165
|
+
private subagentUsage;
|
|
166
|
+
private commitBuffered;
|
|
167
|
+
/**
|
|
168
|
+
* Validate (fold) then append atomically, then publish. Buffered deltas are
|
|
169
|
+
* flushed first so ordering is preserved.
|
|
170
|
+
*/
|
|
171
|
+
private commit;
|
|
172
|
+
private requireTurn;
|
|
173
|
+
private assertOpen;
|
|
174
|
+
private touch;
|
|
175
|
+
}
|
|
176
|
+
export {};
|