specrails-core 6.6.5 → 6.7.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.d.ts +5 -2
- package/dist/agent-runtime/capabilities.js +8 -2
- package/dist/agent-runtime/change-scope.d.ts +2 -0
- package/dist/agent-runtime/change-scope.js +5 -0
- package/dist/agent-runtime/cli-executor.js +7 -0
- package/dist/agent-runtime/cli.d.ts +6 -1
- package/dist/agent-runtime/cli.js +15 -4
- package/dist/agent-runtime/compact/reviewer.js +3 -1
- package/dist/agent-runtime/config.js +16 -5
- package/dist/agent-runtime/efficiency-summary.d.ts +7 -0
- package/dist/agent-runtime/efficiency-summary.js +7 -1
- package/dist/agent-runtime/engine/__fixtures__/implementation-fixture.js +1 -1
- package/dist/agent-runtime/engine/checkpoint/database.d.ts +9 -0
- package/dist/agent-runtime/engine/checkpoint/database.js +40 -3
- package/dist/agent-runtime/engine/checkpoint/ledger.d.ts +1 -0
- package/dist/agent-runtime/engine/checkpoint/ledger.js +7 -2
- package/dist/agent-runtime/engine/cli.d.ts +12 -0
- package/dist/agent-runtime/engine/cli.js +15 -4
- package/dist/agent-runtime/engine/definition-schema.d.ts +1 -1
- package/dist/agent-runtime/engine/definition-schema.js +1 -0
- package/dist/agent-runtime/engine/definition-types.d.ts +1 -1
- package/dist/agent-runtime/engine/definition-types.js +1 -1
- package/dist/agent-runtime/engine/definition-validator.js +8 -0
- package/dist/agent-runtime/engine/efficiency-summary.d.ts +7 -0
- package/dist/agent-runtime/engine/graph-description.d.ts +3 -3
- package/dist/agent-runtime/engine/invocation-context.js +2 -0
- package/dist/agent-runtime/engine/piece-registry.d.ts +1 -1
- package/dist/agent-runtime/engine/piece-registry.js +1 -1
- package/dist/agent-runtime/engine/pieces/artifact-contract.d.ts +5 -0
- package/dist/agent-runtime/engine/pieces/artifact-contract.js +6 -2
- package/dist/agent-runtime/engine/pieces/derivation-coverage.d.ts +30 -0
- package/dist/agent-runtime/engine/pieces/derivation-coverage.js +95 -0
- package/dist/agent-runtime/engine/pieces/derive-change.d.ts +50 -0
- package/dist/agent-runtime/engine/pieces/derive-change.js +209 -0
- package/dist/agent-runtime/engine/pieces/index.js +2 -1
- package/dist/agent-runtime/engine/pieces/openspec.d.ts +16 -1
- package/dist/agent-runtime/engine/pieces/openspec.js +39 -30
- package/dist/agent-runtime/engine/pieces/role-turn.js +26 -5
- package/dist/agent-runtime/engine/preflight.d.ts +2 -1
- package/dist/agent-runtime/engine/preflight.js +3 -1
- package/dist/agent-runtime/engine/regex.d.ts +12 -2
- package/dist/agent-runtime/engine/regex.js +25 -11
- package/dist/agent-runtime/engine/run-status.d.ts +34 -0
- package/dist/agent-runtime/engine/run-status.js +7 -1
- package/dist/agent-runtime/engine/runs.d.ts +56 -0
- package/dist/agent-runtime/engine/runs.js +88 -5
- package/dist/agent-runtime/engine/steering/inbox.d.ts +15 -0
- package/dist/agent-runtime/engine/steering/inbox.js +21 -1
- package/dist/agent-runtime/engine/storage/private-path.js +7 -1
- package/dist/agent-runtime/evaluation.js +1 -1
- package/dist/agent-runtime/executor-types.d.ts +19 -0
- package/dist/agent-runtime/executor-types.js +10 -0
- package/dist/agent-runtime/failure-classification.d.ts +51 -0
- package/dist/agent-runtime/failure-classification.js +110 -0
- package/dist/agent-runtime/graph/artifacts.d.ts +3 -1
- package/dist/agent-runtime/graph/artifacts.js +5 -1
- package/dist/agent-runtime/graph/baseline.d.ts +50 -0
- package/dist/agent-runtime/graph/baseline.js +145 -0
- package/dist/agent-runtime/graph/capability-map.d.ts +29 -0
- package/dist/agent-runtime/graph/capability-map.js +65 -0
- package/dist/agent-runtime/graph/clarifications.d.ts +75 -0
- package/dist/agent-runtime/graph/clarifications.js +158 -0
- package/dist/agent-runtime/graph/nodes.js +178 -19
- package/dist/agent-runtime/graph/review-brief-record.d.ts +25 -0
- package/dist/agent-runtime/graph/review-brief-record.js +26 -0
- package/dist/agent-runtime/graph/review-brief.d.ts +73 -0
- package/dist/agent-runtime/graph/review-brief.js +127 -0
- package/dist/agent-runtime/graph/roles.js +5 -3
- package/dist/agent-runtime/graph/spec-trace-record.d.ts +33 -0
- package/dist/agent-runtime/graph/spec-trace-record.js +65 -0
- package/dist/agent-runtime/graph/state.d.ts +8 -0
- package/dist/agent-runtime/index.d.ts +7 -0
- package/dist/agent-runtime/index.js +7 -0
- package/dist/agent-runtime/openspec.d.ts +1 -0
- package/dist/agent-runtime/openspec.js +1 -1
- package/dist/agent-runtime/project-rules.d.ts +139 -0
- package/dist/agent-runtime/project-rules.js +683 -0
- package/dist/agent-runtime/prompts.d.ts +32 -1
- package/dist/agent-runtime/prompts.js +180 -7
- package/dist/agent-runtime/role-routing.d.ts +2 -1
- package/dist/agent-runtime/session/application/active-session.d.ts +43 -2
- package/dist/agent-runtime/session/application/active-session.js +163 -7
- package/dist/agent-runtime/session/application/session-service.d.ts +2 -2
- package/dist/agent-runtime/session/domain/descriptor.js +5 -0
- package/dist/agent-runtime/session/domain/interruption.js +1 -0
- package/dist/agent-runtime/session/domain/policy.js +2 -0
- package/dist/agent-runtime/session/domain/redaction.d.ts +19 -0
- package/dist/agent-runtime/session/domain/redaction.js +71 -0
- package/dist/agent-runtime/session/domain/types.d.ts +17 -1
- package/dist/agent-runtime/session/drivers/claude/argv.d.ts +14 -0
- package/dist/agent-runtime/session/drivers/claude/argv.js +19 -2
- package/dist/agent-runtime/session/drivers/claude/driver.js +11 -1
- package/dist/agent-runtime/session/drivers/claude/translator.d.ts +4 -0
- package/dist/agent-runtime/session/drivers/claude/translator.js +16 -3
- package/dist/agent-runtime/session/drivers/codex/driver.js +2 -0
- package/dist/agent-runtime/session/drivers/executor/driver.js +2 -0
- package/dist/agent-runtime/session/host/protocol.d.ts +12 -0
- package/dist/agent-runtime/session/host/protocol.js +10 -2
- package/dist/agent-runtime/session/host/server.js +6 -5
- package/dist/agent-runtime/session/index.d.ts +1 -1
- package/dist/agent-runtime/session/journal/sqlite-journal.js +6 -2
- package/dist/agent-runtime/session/ports.d.ts +6 -2
- package/dist/agent-runtime/session/testing/driver-conformance.js +16 -2
- package/dist/agent-runtime/session/testing/memory-journal.js +4 -2
- package/dist/installer/cli.js +10 -1
- package/dist/installer/commands/init.js +14 -3
- package/dist/installer/phases/scaffold.js +56 -5
- package/dist/pipeline/pipeline-state.d.ts +15 -0
- package/dist/pipeline/pipeline-state.js +36 -3
- package/dist/pipeline/spec-trace.d.ts +134 -0
- package/dist/pipeline/spec-trace.js +392 -0
- package/dist/shared/secret-redaction.d.ts +42 -0
- package/dist/shared/secret-redaction.js +184 -0
- package/integration-contract.json +177 -7
- package/package.json +7 -7
- package/schemas/agent-runtime.schema.json +3 -1
- package/schemas/workflow-definition.schema.json +1 -0
- package/templates/settings/codex-config.toml +3 -2
|
@@ -19,6 +19,8 @@ export function executorDescriptor(id, displayName) {
|
|
|
19
19
|
autonomousContinuation: false,
|
|
20
20
|
steer: false,
|
|
21
21
|
toolFiltering: false,
|
|
22
|
+
// No turn-level stop that keeps a session: abort the invocation, then run the input next.
|
|
23
|
+
interrupt: 'emulated',
|
|
22
24
|
usage: Object.freeze({ costUsd: 'per-turn', tokens: 'per-turn' }),
|
|
23
25
|
}),
|
|
24
26
|
});
|
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
import { type ValidateFunction } from 'ajv/dist/2020.js';
|
|
2
2
|
/** Protocol versions this host speaks (docs/agent-sessions/protocol.md). */
|
|
3
3
|
export declare const SESSION_PROTOCOL_VERSIONS: readonly number[];
|
|
4
|
+
/** `session.send` deliveries this host accepts (mirrored into integration-contract.json). */
|
|
5
|
+
export declare const SESSION_DELIVERIES: readonly ["queue", "steer", "interrupt"];
|
|
6
|
+
/**
|
|
7
|
+
* Capabilities `initialize` advertises (mirrored into integration-contract.json).
|
|
8
|
+
* `delegation`: session.delegate / session.waitSubagents and subagentRuntime.mode 'delegated'.
|
|
9
|
+
* `flowSessions`: session.send delivery 'interrupt' and per-driver capabilities.interrupt.
|
|
10
|
+
*/
|
|
11
|
+
export declare const SESSION_HOST_CAPABILITIES: Readonly<{
|
|
12
|
+
readonly sessions: 1;
|
|
13
|
+
readonly delegation: 1;
|
|
14
|
+
readonly flowSessions: 1;
|
|
15
|
+
}>;
|
|
4
16
|
/** Longest accepted request line. */
|
|
5
17
|
export declare const MAX_FRAME_BYTES: number;
|
|
6
18
|
type Schema = Record<string, unknown>;
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { Ajv2020 } from 'ajv/dist/2020.js';
|
|
2
2
|
/** Protocol versions this host speaks (docs/agent-sessions/protocol.md). */
|
|
3
3
|
export const SESSION_PROTOCOL_VERSIONS = Object.freeze([1]);
|
|
4
|
+
/** `session.send` deliveries this host accepts (mirrored into integration-contract.json). */
|
|
5
|
+
export const SESSION_DELIVERIES = Object.freeze(['queue', 'steer', 'interrupt']);
|
|
6
|
+
/**
|
|
7
|
+
* Capabilities `initialize` advertises (mirrored into integration-contract.json).
|
|
8
|
+
* `delegation`: session.delegate / session.waitSubagents and subagentRuntime.mode 'delegated'.
|
|
9
|
+
* `flowSessions`: session.send delivery 'interrupt' and per-driver capabilities.interrupt.
|
|
10
|
+
*/
|
|
11
|
+
export const SESSION_HOST_CAPABILITIES = Object.freeze({ sessions: 1, delegation: 1, flowSessions: 1 });
|
|
4
12
|
/** Longest accepted request line. */
|
|
5
13
|
export const MAX_FRAME_BYTES = 2 * 1024 * 1024;
|
|
6
14
|
const id = { type: 'string', minLength: 1, maxLength: 200 };
|
|
@@ -20,7 +28,7 @@ const mcpServer = {
|
|
|
20
28
|
};
|
|
21
29
|
const limits = {
|
|
22
30
|
type: 'object', additionalProperties: false,
|
|
23
|
-
properties: Object.fromEntries(['idleMs', 'stallMs', 'backgroundMaxMs', 'turnInactivityMs', 'maxSettleHandoffs', 'settleDebounceMs'].map((key) => [key, { type: 'integer', minimum: 0 }])),
|
|
31
|
+
properties: Object.fromEntries(['idleMs', 'stallMs', 'backgroundMaxMs', 'turnInactivityMs', 'toolInactivityMs', 'maxSettleHandoffs', 'settleDebounceMs'].map((key) => [key, { type: 'integer', minimum: 0 }])),
|
|
24
32
|
};
|
|
25
33
|
const policy = {
|
|
26
34
|
type: 'object', additionalProperties: false, required: ['subagents'],
|
|
@@ -58,7 +66,7 @@ export const METHOD_SCHEMAS = Object.freeze({
|
|
|
58
66
|
},
|
|
59
67
|
oneOf: [{ required: ['resume'] }, { required: ['driver', 'model', 'cwd', 'policy'] }],
|
|
60
68
|
},
|
|
61
|
-
'session.send': { type: 'object', additionalProperties: false, required: ['sessionId', 'input'], properties: { sessionId: id, input: { type: 'object', additionalProperties: false, required: ['inputId', 'text', 'delivery'], properties: { inputId: id, text, attachments: { type: 'array', maxItems: 32, items: attachment }, delivery: { enum: [
|
|
69
|
+
'session.send': { type: 'object', additionalProperties: false, required: ['sessionId', 'input'], properties: { sessionId: id, input: { type: 'object', additionalProperties: false, required: ['inputId', 'text', 'delivery'], properties: { inputId: id, text, attachments: { type: 'array', maxItems: 32, items: attachment }, delivery: { enum: [...SESSION_DELIVERIES] } } } } },
|
|
62
70
|
'session.interrupt': { type: 'object', additionalProperties: false, required: ['sessionId'], properties: { sessionId: id } },
|
|
63
71
|
'session.stopSubagents': { type: 'object', additionalProperties: false, required: ['sessionId'], properties: { sessionId: id, subagentIds: { type: 'array', maxItems: 500, items: id } } },
|
|
64
72
|
'session.delegate': { type: 'object', additionalProperties: false, required: ['sessionId', 'description', 'prompt'], properties: { sessionId: id, description: { type: 'string', minLength: 1, maxLength: 200 }, prompt: { type: 'string', minLength: 1, maxLength: 64 * 1024 }, agentType: { type: 'string', minLength: 1, maxLength: 100 }, contextTurns: { type: 'integer', minimum: 0, maximum: 10 } } },
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { StringDecoder } from 'node:string_decoder';
|
|
2
2
|
import { SessionError, isSessionError } from '../domain/errors.js';
|
|
3
|
-
import {
|
|
3
|
+
import { redactSessionSnapshot } from '../domain/redaction.js';
|
|
4
|
+
import { MAX_FRAME_BYTES, RPC_APPLICATION_ERROR, RPC_INVALID_PARAMS, RPC_INVALID_REQUEST, RPC_METHOD_NOT_FOUND, RPC_PARSE_ERROR, SESSION_HOST_CAPABILITIES, SESSION_PROTOCOL_VERSIONS, validatorFor, } from './protocol.js';
|
|
4
5
|
/**
|
|
5
6
|
* JSON-RPC 2.0 server over NDJSON stdio (session protocol v1).
|
|
6
7
|
* One handler per method (closed table); each validates its params with a
|
|
@@ -27,7 +28,8 @@ export class SessionHost {
|
|
|
27
28
|
const service = runtime.service;
|
|
28
29
|
this.handlers = Object.freeze({
|
|
29
30
|
'initialize': async (params) => this.initialize(params),
|
|
30
|
-
|
|
31
|
+
// Live snapshots come from the in-memory session: redact them like the journal does.
|
|
32
|
+
'session.open': async (params) => { const opened = await service.open(params); return { ...opened, snapshot: redactSessionSnapshot(opened.snapshot) }; },
|
|
31
33
|
'session.send': async (params) => service.send(String(params.sessionId), params.input),
|
|
32
34
|
'session.interrupt': async (params) => service.interrupt(String(params.sessionId)),
|
|
33
35
|
'session.delegate': async (params) => service.delegate(String(params.sessionId), {
|
|
@@ -40,7 +42,7 @@ export class SessionHost {
|
|
|
40
42
|
'session.stopSubagents': async (params) => service.stopSubagents(String(params.sessionId), params.subagentIds),
|
|
41
43
|
'session.update': async (params) => { const { sessionId, ...changes } = params; return service.update(String(sessionId), changes); },
|
|
42
44
|
'session.close': async (params) => { await service.close(String(params.sessionId), String(params.reason)); return {}; },
|
|
43
|
-
'session.snapshot': async (params) => service.snapshot(String(params.sessionId)),
|
|
45
|
+
'session.snapshot': async (params) => redactSessionSnapshot(service.snapshot(String(params.sessionId))),
|
|
44
46
|
'session.events': async (params) => service.events(String(params.sessionId), Number(params.afterSeq), params.limit === undefined ? undefined : Number(params.limit)),
|
|
45
47
|
'session.list': async (params) => ({ sessions: service.list(params.state ?? 'open') }),
|
|
46
48
|
'host.ping': async () => ({ ...service.stats(), uptimeMs: Math.round(process.uptime() * 1000) }),
|
|
@@ -159,8 +161,7 @@ export class SessionHost {
|
|
|
159
161
|
protocolVersion: Math.max(...requested),
|
|
160
162
|
scope: this.runtime.scope,
|
|
161
163
|
runtime: this.runtime.identity,
|
|
162
|
-
|
|
163
|
-
capabilities: { sessions: 1, delegation: 1 },
|
|
164
|
+
capabilities: { ...SESSION_HOST_CAPABILITIES },
|
|
164
165
|
drivers: this.runtime.service.drivers(),
|
|
165
166
|
};
|
|
166
167
|
}
|
|
@@ -7,7 +7,7 @@ import type { Clock, DriverCatalog, Ids } from './ports.js';
|
|
|
7
7
|
export { SessionError, isSessionError, type SessionErrorCode } from './domain/errors.js';
|
|
8
8
|
export { SESSION_EVENT_TYPES, type SessionEvent, type SessionEventBody, type SessionEventEnvelope, type SessionEventType, type ToolActivity } from './domain/events.js';
|
|
9
9
|
export type { SessionSnapshot, SubagentNode, TurnRecord, InputRecord, OpenTurn } from './domain/snapshot.js';
|
|
10
|
-
export type { DriverCapabilities, DriverDescriptor, SessionLimits, SessionPolicy, Usage, McpServerSpec, Attachment } from './domain/types.js';
|
|
10
|
+
export type { DriverCapabilities, DriverDescriptor, InputDelivery, SessionLimits, SessionPolicy, Usage, McpServerSpec, Attachment } from './domain/types.js';
|
|
11
11
|
export type { SessionPolicyInput } from './domain/policy.js';
|
|
12
12
|
export { DEFAULT_LIMITS } from './domain/policy.js';
|
|
13
13
|
export { SessionService, type OpenParams, type SendParams } from './application/session-service.js';
|
|
@@ -4,6 +4,7 @@ import { DatabaseSync } from 'node:sqlite';
|
|
|
4
4
|
import { sessionsRoot } from '../../../shared/specrails-home.js';
|
|
5
5
|
import { PersistenceError, privateSqlitePath } from '../../engine/storage/private-path.js';
|
|
6
6
|
import { SessionError } from '../domain/errors.js';
|
|
7
|
+
import { redactSessionEvent, redactSessionSnapshot } from '../domain/redaction.js';
|
|
7
8
|
import { emptySnapshot, foldEvents } from '../domain/snapshot.js';
|
|
8
9
|
import { JOURNAL_MIGRATIONS, JOURNAL_SCHEMA_VERSION } from './schema.js';
|
|
9
10
|
export const JOURNAL_LEASE_MS = 60_000;
|
|
@@ -82,8 +83,11 @@ export class SqliteSessionJournal {
|
|
|
82
83
|
const current = this.loadSnapshot(sessionId);
|
|
83
84
|
if (!current)
|
|
84
85
|
throw new SessionError('session_not_found', `Unknown session ${sessionId}`);
|
|
85
|
-
|
|
86
|
-
|
|
86
|
+
// Fold the events as produced (input fingerprints must match the live
|
|
87
|
+
// session), then persist and return only redacted copies: what is
|
|
88
|
+
// stored, published and replayed is the same redacted stream.
|
|
89
|
+
const next = redactSessionSnapshot(foldEvents(current, events.map((event, index) => ({ sessionId, seq: current.lastSeq + index + 1, event }))));
|
|
90
|
+
const envelopes = events.map((event, index) => ({ sessionId, seq: current.lastSeq + index + 1, event: redactSessionEvent(event) }));
|
|
87
91
|
const insert = this.db.prepare('INSERT INTO events(session_id,seq,type,at,payload_json) VALUES (?,?,?,?,?)');
|
|
88
92
|
for (const envelope of envelopes)
|
|
89
93
|
insert.run(sessionId, envelope.seq, envelope.event.type, envelope.event.at, JSON.stringify(envelope.event));
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import type { SessionEvent, SessionEventEnvelope, ToolActivity } from './domain/events.js';
|
|
9
9
|
import type { SessionSnapshot } from './domain/snapshot.js';
|
|
10
|
-
import type { Attachment, DriverDescriptor, OutputChannel, SessionPolicy, SubagentKind, SubagentPhase } from './domain/types.js';
|
|
10
|
+
import type { Attachment, DriverDescriptor, InputDelivery, OutputChannel, SessionPolicy, SubagentKind, SubagentPhase } from './domain/types.js';
|
|
11
11
|
import type { ReportedUsage, UsageBaseline } from './domain/usage.js';
|
|
12
12
|
export interface DriverOpenSpec {
|
|
13
13
|
sessionId: string;
|
|
@@ -25,7 +25,11 @@ export interface DriverInput {
|
|
|
25
25
|
inputId: string;
|
|
26
26
|
text: string;
|
|
27
27
|
attachments?: Attachment[];
|
|
28
|
-
|
|
28
|
+
/**
|
|
29
|
+
* Drivers receive `interrupt` only when they declare a native queue and a
|
|
30
|
+
* native interrupt: they stop the running turn and put this input first.
|
|
31
|
+
*/
|
|
32
|
+
delivery: InputDelivery;
|
|
29
33
|
/** Set by the application for policy-driven turns (e.g. collecting settled sub-agents). */
|
|
30
34
|
origin?: 'user' | 'system';
|
|
31
35
|
}
|
|
@@ -8,12 +8,12 @@ function assert(condition, message) {
|
|
|
8
8
|
if (!condition)
|
|
9
9
|
throw new Error(`Driver conformance: ${message}`);
|
|
10
10
|
}
|
|
11
|
-
async function oneTurn(harness) {
|
|
11
|
+
async function oneTurn(harness, delivery = 'queue') {
|
|
12
12
|
const factory = harness.factory();
|
|
13
13
|
const journal = new MemoryJournal();
|
|
14
14
|
const service = new SessionService({ journal, drivers: catalogOf(factory), clock: new FakeClock(), ids: new SequentialIds() });
|
|
15
15
|
const { sessionId } = await service.open({ driver: factory.descriptor.id, model: harness.model, cwd: process.cwd(), policy: { subagents: harness.subagents } });
|
|
16
|
-
await service.send(sessionId, { inputId: 'conformance-1', text: 'Reply briefly.', delivery
|
|
16
|
+
await service.send(sessionId, { inputId: 'conformance-1', text: 'Reply briefly.', delivery });
|
|
17
17
|
await harness.settle(factory);
|
|
18
18
|
const events = () => journal.events(sessionId).map((envelope) => envelope.event);
|
|
19
19
|
return { factory, journal, service, sessionId, events };
|
|
@@ -67,6 +67,20 @@ export const DRIVER_CONFORMANCE = Object.freeze([
|
|
|
67
67
|
assert(snapshot.openTurn === null && snapshot.phase === 'idle', 'the session is idle after a turn without sub-agents');
|
|
68
68
|
},
|
|
69
69
|
},
|
|
70
|
+
{
|
|
71
|
+
name: 'declares its interrupt mechanism and treats an interrupt on an idle session as a queued input',
|
|
72
|
+
async run(harness) {
|
|
73
|
+
const caps = harness.factory().descriptor.capabilities;
|
|
74
|
+
assert(caps.interrupt === 'native' || caps.interrupt === 'emulated', 'capabilities.interrupt is native or emulated');
|
|
75
|
+
const { events, service, sessionId } = await oneTurn(harness, 'interrupt');
|
|
76
|
+
const list = events();
|
|
77
|
+
const accepted = list.find((event) => event.type === 'input.accepted');
|
|
78
|
+
assert(accepted?.delivery === 'interrupt', 'the journal keeps the requested delivery');
|
|
79
|
+
const statuses = list.filter((event) => event.type === 'turn.completed').map((event) => event.status);
|
|
80
|
+
assert(statuses.length === 1 && statuses[0] === 'completed', 'nothing is interrupted on an idle session');
|
|
81
|
+
assert(service.snapshot(sessionId).inputs['conformance-1']?.state === 'completed', 'the input completes with its turn');
|
|
82
|
+
},
|
|
83
|
+
},
|
|
70
84
|
{
|
|
71
85
|
name: 'reports usage according to its declared semantics',
|
|
72
86
|
async run(harness) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { redactSessionEvent, redactSessionSnapshot } from '../domain/redaction.js';
|
|
1
2
|
import { emptySnapshot, foldEvents } from '../domain/snapshot.js';
|
|
2
3
|
/**
|
|
3
4
|
* In-memory SessionJournal with the same contract as the SQLite adapter:
|
|
@@ -23,8 +24,9 @@ export class MemoryJournal {
|
|
|
23
24
|
this.failNextAppend = null;
|
|
24
25
|
throw error;
|
|
25
26
|
}
|
|
26
|
-
const
|
|
27
|
-
|
|
27
|
+
const snapshot = redactSessionSnapshot(foldEvents(entry.snapshot, events.map((event, index) => ({ sessionId, seq: entry.snapshot.lastSeq + index + 1, event }))));
|
|
28
|
+
// Same redaction boundary as the SQLite adapter (domain/redaction.ts).
|
|
29
|
+
const envelopes = events.map((event, index) => ({ sessionId, seq: entry.snapshot.lastSeq + index + 1, event: structuredClone(redactSessionEvent(event)) }));
|
|
28
30
|
entry.events.push(...envelopes);
|
|
29
31
|
entry.snapshot = snapshot;
|
|
30
32
|
if (options?.baseline)
|
package/dist/installer/cli.js
CHANGED
|
@@ -25,7 +25,8 @@ function usageText() {
|
|
|
25
25
|
' install-framework Materialize the versioned framework (offline)',
|
|
26
26
|
' swap-current Point framework/current at a version (offline)',
|
|
27
27
|
' assemble Link the framework into a project workspace (offline)',
|
|
28
|
-
' pipeline Inspect and verify durable implementation phases',
|
|
28
|
+
' pipeline Inspect and verify durable implementation phases (pipeline trace: requirement ↔ test traceability)',
|
|
29
|
+
' rules Inventory project gates and written rules (rules inventory → .specrails/rules.json)',
|
|
29
30
|
' runtime Run, inspect and resume programmatic agents',
|
|
30
31
|
' help Show this help message',
|
|
31
32
|
' version Print the installed version',
|
|
@@ -57,6 +58,8 @@ async function dispatch(subcommand, flags, positionals) {
|
|
|
57
58
|
return runPipelineCommand(flags, positionals);
|
|
58
59
|
case 'runtime':
|
|
59
60
|
return (await import('../agent-runtime/cli.js')).runRuntimeCommand(flags, positionals);
|
|
61
|
+
case 'rules':
|
|
62
|
+
return (await import('../agent-runtime/project-rules.js')).runRulesCommand(flags, positionals);
|
|
60
63
|
case 'install-framework':
|
|
61
64
|
await runInstallFramework(flags);
|
|
62
65
|
return 0;
|
|
@@ -79,6 +82,12 @@ async function dispatch(subcommand, flags, positionals) {
|
|
|
79
82
|
}
|
|
80
83
|
}
|
|
81
84
|
export async function main(argv = process.argv.slice(2)) {
|
|
85
|
+
// `pipeline trace` keeps its own argv (repeatable, comma-separated lists) and
|
|
86
|
+
// judges execution with the runtime's full test-reachability check.
|
|
87
|
+
if (argv[0] === 'pipeline' && argv[1] === 'trace') {
|
|
88
|
+
const [{ runSpecTraceCli }, { commandReachesTest }] = await Promise.all([import('../pipeline/spec-trace.js'), import('../agent-runtime/compact/test-reachability.js')]);
|
|
89
|
+
return runSpecTraceCli(argv.slice(2), { reaches: commandReachesTest });
|
|
90
|
+
}
|
|
82
91
|
const { subcommand, flags, positionals } = parseArgs(argv);
|
|
83
92
|
// `--version` is global only before a command: `swap-current --version <v>`
|
|
84
93
|
// names a framework version.
|
|
@@ -6,7 +6,7 @@ import { recordSuccessfulInstall } from '../util/registry.js';
|
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
7
|
import { InstallerError } from '../util/errors.js';
|
|
8
8
|
import { runCommand } from '../util/exec.js';
|
|
9
|
-
import { info, ok, step } from '../util/logger.js';
|
|
9
|
+
import { info, ok, step, warn } from '../util/logger.js';
|
|
10
10
|
import { isDir, isSymlink, listDir, pathExists, readTextFile, removePath, writeFileLf, } from '../util/fs.js';
|
|
11
11
|
import { loadInstallConfig, resolveConfigPath, } from '../phases/install-config.js';
|
|
12
12
|
import { checkPrerequisites } from '../phases/prereqs.js';
|
|
@@ -188,8 +188,8 @@ export async function runInit(flags) {
|
|
|
188
188
|
assertNoCoreDowngrade(readTextFile(installedMarker).trim(), version);
|
|
189
189
|
const previousSelections = snapshotWorkspaceProviderSelections(artifactRoot);
|
|
190
190
|
const surfaces = [
|
|
191
|
-
...['.claude', '.codex', '.gemini', '.kimi-code', 'AGENTS.md', 'GEMINI.md', '.gitignore'].map((name) => path.join(artifactRoot, name)),
|
|
192
|
-
...['specrails-version', 'specrails-manifest.json', 'setup-templates', 'runtime'].map((name) => path.join(artifactRoot, '.specrails', name)),
|
|
191
|
+
...['.claude', '.codex', '.gemini', '.kimi-code', 'AGENTS.md', 'GEMINI.md', 'CLAUDE.md', '.gitignore'].map((name) => path.join(artifactRoot, name)),
|
|
192
|
+
...['specrails-version', 'specrails-manifest.json', 'setup-templates', 'runtime', 'rules.json'].map((name) => path.join(artifactRoot, '.specrails', name)),
|
|
193
193
|
path.join(fwDir, 'current'),
|
|
194
194
|
// Contents deliberately NOT snapshotted: `installFramework` rebuilds a
|
|
195
195
|
// version tree whenever its content stamp does not match and stages
|
|
@@ -227,6 +227,17 @@ export async function runInit(flags) {
|
|
|
227
227
|
}
|
|
228
228
|
// openspec STAYS in the repo (codeRoot) — unchanged behaviour.
|
|
229
229
|
await installOpenSpecProject(codeRoot, prereqs.provider, artifactRoot);
|
|
230
|
+
// Project rules inventory (gates + parsed rule statements). Offline here:
|
|
231
|
+
// init never spends on a provider; classifications from an earlier
|
|
232
|
+
// `specrails-core rules inventory` are kept for unchanged rules.
|
|
233
|
+
try {
|
|
234
|
+
const { runRulesInventory } = await import('../../agent-runtime/project-rules.js');
|
|
235
|
+
const { inventory } = await runRulesInventory({ root: codeRoot, artifactRoot, classify: false });
|
|
236
|
+
ok(`Project rules inventory: ${inventory.gates.length} gate(s), ${inventory.rules.length} rule(s) in .specrails/rules.json`);
|
|
237
|
+
}
|
|
238
|
+
catch (err) {
|
|
239
|
+
warn(`Project rules inventory skipped: ${err.message}`);
|
|
240
|
+
}
|
|
230
241
|
if (artifactRoot !== codeRoot)
|
|
231
242
|
recordSuccessfulInstall(repoRoot, { home: process.env.SPECRAILS_REGISTRY_HOME, providers: [prereqs.provider], coreVersion: version });
|
|
232
243
|
step('Installation complete');
|
|
@@ -202,8 +202,15 @@ export function scaffoldInstallation(input) {
|
|
|
202
202
|
if (skills.placed > 0)
|
|
203
203
|
info(`Placed ${skills.placed} skill(s) into ${input.providerDir}/skills/`);
|
|
204
204
|
}
|
|
205
|
-
// ---
|
|
206
|
-
if (input.provider === '
|
|
205
|
+
// --- Provider settings + managed instruction block ---
|
|
206
|
+
if (input.provider === 'claude') {
|
|
207
|
+
const written = applyClaudeSettings(input);
|
|
208
|
+
copiedFiles += written;
|
|
209
|
+
if (written > 0) {
|
|
210
|
+
info(`Claude provider: wrote ${written} setting file(s) (CLAUDE.md)`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
else if (input.provider === 'codex') {
|
|
207
214
|
const written = applyCodexSettings(input);
|
|
208
215
|
copiedFiles += written;
|
|
209
216
|
if (written > 0) {
|
|
@@ -544,7 +551,10 @@ function seedProjectLayer(input) {
|
|
|
544
551
|
// Project-named instruction file (codex AGENTS.md / gemini GEMINI.md). Reuse
|
|
545
552
|
// the same sentinel-upsert helpers via the settings appliers, scoped so they
|
|
546
553
|
// ONLY emit the instruction file (the settings file is already linked above).
|
|
547
|
-
if (input.provider === '
|
|
554
|
+
if (input.provider === 'claude') {
|
|
555
|
+
seedInstructionFile(path.join(input.workspace, 'CLAUDE.md'), renderInitialClaudeMd(input.codeRoot));
|
|
556
|
+
}
|
|
557
|
+
else if (input.provider === 'codex') {
|
|
548
558
|
seedInstructionFile(path.join(input.workspace, 'AGENTS.md'), renderInitialAgentsMd(input.codeRoot));
|
|
549
559
|
}
|
|
550
560
|
else if (input.provider === 'gemini') {
|
|
@@ -991,6 +1001,39 @@ function renderInitialKimiAgentsMd(repoRoot) {
|
|
|
991
1001
|
'',
|
|
992
1002
|
].join('\n');
|
|
993
1003
|
}
|
|
1004
|
+
/**
|
|
1005
|
+
* Claude Code provider settings: the sentinel-protected Specrails block in the
|
|
1006
|
+
* root `CLAUDE.md` (the instruction file Claude Code loads on startup), upserted
|
|
1007
|
+
* exactly like codex `AGENTS.md` and gemini `GEMINI.md` so user content outside
|
|
1008
|
+
* the block survives. Returns files written.
|
|
1009
|
+
*/
|
|
1010
|
+
function applyClaudeSettings(input) {
|
|
1011
|
+
const claudeMdPath = path.join(input.artifactRoot, 'CLAUDE.md');
|
|
1012
|
+
const content = renderInitialClaudeMd(input.codeRoot);
|
|
1013
|
+
if (!pathExists(claudeMdPath)) {
|
|
1014
|
+
writeFileLf(claudeMdPath, content);
|
|
1015
|
+
return 1;
|
|
1016
|
+
}
|
|
1017
|
+
const existing = readTextFile(claudeMdPath);
|
|
1018
|
+
const next = upsertAgentsMdManagedBlock(existing, extractManagedBlock(content));
|
|
1019
|
+
if (next === existing)
|
|
1020
|
+
return 0;
|
|
1021
|
+
writeFileLf(claudeMdPath, next);
|
|
1022
|
+
return 1;
|
|
1023
|
+
}
|
|
1024
|
+
function renderInitialClaudeMd(repoRoot) {
|
|
1025
|
+
return renderInitialAgentsMd(repoRoot).replace(AGENTS_MD_END, [
|
|
1026
|
+
'## Project rules',
|
|
1027
|
+
'',
|
|
1028
|
+
'- `.specrails/rules.json` (refreshed by `specrails-core rules inventory`)',
|
|
1029
|
+
' lists the gates this repository already enforces and how each written',
|
|
1030
|
+
' rule is checked. Run the gates that cover your change; rules marked',
|
|
1031
|
+
' `humanOnly` are for the human reviewer, never claim them verified.',
|
|
1032
|
+
'- Read provider rules from `.claude/rules/` when present.',
|
|
1033
|
+
'',
|
|
1034
|
+
AGENTS_MD_END,
|
|
1035
|
+
].join('\n'));
|
|
1036
|
+
}
|
|
994
1037
|
function renderInitialGeminiMd(repoRoot) {
|
|
995
1038
|
return renderInitialAgentsMd(repoRoot).replace(AGENTS_MD_END, 'Read provider rules from `.gemini/`.\n\n' + AGENTS_MD_END);
|
|
996
1039
|
}
|
|
@@ -1010,6 +1053,14 @@ function placePipelineRuntime(input) {
|
|
|
1010
1053
|
return 0;
|
|
1011
1054
|
const dest = path.join(input.artifactRoot, '.specrails', 'runtime');
|
|
1012
1055
|
copyFile(source, path.join(dest, 'pipeline-state.mjs'));
|
|
1056
|
+
// Requirement ↔ test traceability, self-contained like pipeline-state: `node .specrails/runtime/trace.mjs --check`.
|
|
1057
|
+
const trace = path.join(input.scriptDir, 'dist', 'pipeline', 'spec-trace.js');
|
|
1058
|
+
const traced = pathExists(trace) ? 2 : 0;
|
|
1059
|
+
if (traced) {
|
|
1060
|
+
copyFile(trace, path.join(dest, 'spec-trace.mjs'));
|
|
1061
|
+
writeFileLf(path.join(dest, 'trace.mjs'), "import { runSpecTraceCli } from './spec-trace.mjs'\n" +
|
|
1062
|
+
"process.exitCode = await runSpecTraceCli(process.argv.slice(2))\n");
|
|
1063
|
+
}
|
|
1013
1064
|
writeFileLf(path.join(dest, 'pipeline.mjs'), "import { runPipelineCli } from './pipeline-state.mjs'\n" +
|
|
1014
1065
|
"process.exitCode = await runPipelineCli(process.argv.slice(2))\n");
|
|
1015
1066
|
const runtime = path.join(input.scriptDir, 'dist', 'agent-runtime', 'cli.js');
|
|
@@ -1026,9 +1077,9 @@ function placePipelineRuntime(input) {
|
|
|
1026
1077
|
agents: Object.fromEntries(['architect', 'developer', 'reviewer'].map(role => [role, { provider, maxTurns: 100 }])), verification: [],
|
|
1027
1078
|
}, null, 2) + '\n');
|
|
1028
1079
|
}
|
|
1029
|
-
return 3;
|
|
1080
|
+
return 3 + traced;
|
|
1030
1081
|
}
|
|
1031
|
-
return 2;
|
|
1082
|
+
return 2 + traced;
|
|
1032
1083
|
}
|
|
1033
1084
|
function pruneLegacyArtifacts(input) {
|
|
1034
1085
|
const legacyPaths = [
|
|
@@ -147,10 +147,19 @@ export interface AcceptanceCheck {
|
|
|
147
147
|
scope: string;
|
|
148
148
|
limitations: string;
|
|
149
149
|
}
|
|
150
|
+
/** Where in the change one finding applies (contract 5.9). `finding` indexes `findings`; `path` is repository-relative. */
|
|
151
|
+
export interface FindingLocation {
|
|
152
|
+
finding: number;
|
|
153
|
+
path: string;
|
|
154
|
+
line?: number;
|
|
155
|
+
endLine?: number;
|
|
156
|
+
}
|
|
150
157
|
export interface AcceptanceReport {
|
|
151
158
|
criteria: AcceptanceCriterion[];
|
|
152
159
|
checks: AcceptanceCheck[];
|
|
153
160
|
findings: string[];
|
|
161
|
+
/** Optional anchors for findings (contract 5.9); absent when the reviewer named none. */
|
|
162
|
+
findingLocations?: FindingLocation[];
|
|
154
163
|
}
|
|
155
164
|
export interface AcceptanceReceipt extends AcceptanceReport {
|
|
156
165
|
scopeHash: string;
|
|
@@ -276,6 +285,12 @@ declare function inspectReceipt(state: PipelineState, env?: NodeJS.ProcessEnv):
|
|
|
276
285
|
export declare function frozenAcceptanceCriteria(contextInput: unknown): Array<Pick<AcceptanceCriterion, 'specId' | 'criterionIndex' | 'requirement'>>;
|
|
277
286
|
/** Validates a report against the frozen scope without recording it; throws the same errors `recordAcceptance` would. */
|
|
278
287
|
export declare function validateAcceptanceReport(contextInput: unknown, input: unknown): AcceptanceReport;
|
|
288
|
+
/**
|
|
289
|
+
* Keeps only well-formed anchors for the findings that exist. A malformed
|
|
290
|
+
* anchor is dropped, never fatal: a location is advisory and must not fail an
|
|
291
|
+
* otherwise valid review.
|
|
292
|
+
*/
|
|
293
|
+
export declare function sanitizeFindingLocations(value: unknown, findingCount: number): FindingLocation[];
|
|
279
294
|
declare function inspectAcceptance(state: PipelineState): {
|
|
280
295
|
valid: boolean;
|
|
281
296
|
status: PipelineCompletion['validation'];
|
|
@@ -296,7 +296,7 @@ export function bindImplementationExclusions(contextInput, exclusions) {
|
|
|
296
296
|
continue; // A scoped child receives the parent union, filtered to its frozen repositories.
|
|
297
297
|
repositoryExclusions[id] = [...new Set(paths)].sort();
|
|
298
298
|
for (const item of repositoryExclusions[id]) {
|
|
299
|
-
if (!/^(?:[a-zA-Z0-9][a-zA-Z0-9._-]*\/)*openspec\/(?:changes|specs)\/[a-z0-9][a-z0-9./-]
|
|
299
|
+
if (!/^(?:[a-zA-Z0-9][a-zA-Z0-9._-]*\/)*openspec\/(?:(?:changes|specs)\/[a-z0-9][a-z0-9./-]*|capability-map\.json)$/.test(item))
|
|
300
300
|
fail('Implementation artifact exclusion is outside declared OpenSpec artifacts');
|
|
301
301
|
safeChild(repository.path, item);
|
|
302
302
|
}
|
|
@@ -485,7 +485,8 @@ export function initializePipeline(contextInput, change) {
|
|
|
485
485
|
schemaVersion: 1, runId: context.runId, change, context, scopeHash: digest(canonical(context)), revision: 0,
|
|
486
486
|
createdAt: now, updatedAt: now,
|
|
487
487
|
phases: Object.fromEntries(PHASES.map((phase) => [phase, { status: 'pending' }])),
|
|
488
|
-
|
|
488
|
+
// The capability map is Core metadata the archive step rewrites (graph/capability-map.ts), never part of the reviewed candidate.
|
|
489
|
+
artifactExclusions: ['openspec/changes/' + change, 'openspec/capability-map.json'],
|
|
489
490
|
};
|
|
490
491
|
atomicJson(path.join(pipelineStateDirectory(context), 'context.json'), context);
|
|
491
492
|
saveState(state);
|
|
@@ -615,6 +616,36 @@ export function frozenAcceptanceCriteria(contextInput) {
|
|
|
615
616
|
export function validateAcceptanceReport(contextInput, input) {
|
|
616
617
|
return acceptanceReport(validatePipelineContext(contextInput), input);
|
|
617
618
|
}
|
|
619
|
+
const MAX_FINDING_LOCATIONS = 50;
|
|
620
|
+
/**
|
|
621
|
+
* Keeps only well-formed anchors for the findings that exist. A malformed
|
|
622
|
+
* anchor is dropped, never fatal: a location is advisory and must not fail an
|
|
623
|
+
* otherwise valid review.
|
|
624
|
+
*/
|
|
625
|
+
export function sanitizeFindingLocations(value, findingCount) {
|
|
626
|
+
if (!Array.isArray(value))
|
|
627
|
+
return [];
|
|
628
|
+
const out = [];
|
|
629
|
+
const seen = new Set();
|
|
630
|
+
for (const raw of value) {
|
|
631
|
+
if (out.length >= MAX_FINDING_LOCATIONS)
|
|
632
|
+
break;
|
|
633
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
|
|
634
|
+
continue;
|
|
635
|
+
const row = raw;
|
|
636
|
+
const finding = row.finding;
|
|
637
|
+
if (typeof finding !== 'number' || !Number.isInteger(finding) || finding < 0 || finding >= findingCount || seen.has(finding))
|
|
638
|
+
continue;
|
|
639
|
+
const filePath = typeof row.path === 'string' ? row.path.trim().replace(/\\/g, '/').replace(/^\.\//, '') : '';
|
|
640
|
+
if (!filePath || filePath.length > 500 || filePath.startsWith('/') || /^[A-Za-z]:/.test(filePath) || filePath.includes('\0') || filePath.split('/').includes('..'))
|
|
641
|
+
continue;
|
|
642
|
+
const line = typeof row.line === 'number' && Number.isInteger(row.line) && row.line >= 1 && row.line <= 10_000_000 ? row.line : undefined;
|
|
643
|
+
const endLine = line !== undefined && typeof row.endLine === 'number' && Number.isInteger(row.endLine) && row.endLine > line && row.endLine <= 10_000_000 ? row.endLine : undefined;
|
|
644
|
+
seen.add(finding);
|
|
645
|
+
out.push({ finding, path: filePath, ...(line !== undefined ? { line } : {}), ...(endLine !== undefined ? { endLine } : {}) });
|
|
646
|
+
}
|
|
647
|
+
return out;
|
|
648
|
+
}
|
|
618
649
|
function nonempty(value) { return typeof value === 'string' && value.trim().length > 0; }
|
|
619
650
|
function evidence(value) { return Array.isArray(value) && value.length > 0 && value.every(nonempty); }
|
|
620
651
|
function acceptanceReport(context, input) {
|
|
@@ -655,7 +686,9 @@ function acceptanceReport(context, input) {
|
|
|
655
686
|
fail('Check needs status, evidence, measurement scope and limitations');
|
|
656
687
|
return { name: check.name, status: check.status, required: check.required, evidence: check.evidence, scope: check.scope, limitations: check.limitations };
|
|
657
688
|
});
|
|
658
|
-
|
|
689
|
+
const findings = report.findings;
|
|
690
|
+
const findingLocations = sanitizeFindingLocations(report.findingLocations, findings.length);
|
|
691
|
+
return { criteria, checks, findings, ...(findingLocations.length ? { findingLocations } : {}) };
|
|
659
692
|
}
|
|
660
693
|
function inspectAcceptance(state) {
|
|
661
694
|
const receipt = state.acceptance;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
export declare const SPEC_TRACE_SCHEMA_VERSION = 1;
|
|
2
|
+
export declare const SPEC_TRACE_FILE = "spec-trace.json";
|
|
3
|
+
/** A file that IS a test across ecosystems: `x.test.ts`, `x.spec.js`, `test_x.py`, `x_test.go`, `x_spec.rb`, `XTest.java`, `test_x.sh`, `x.bats`. */
|
|
4
|
+
export declare const TEST_FILE: RegExp;
|
|
5
|
+
/** `capability/slug`: lowercase, accents removed (NFKD), every run of non-alphanumerics a single dash, no leading or trailing dash. */
|
|
6
|
+
export declare function requirementSlug(name: string): string;
|
|
7
|
+
export declare function requirementId(capability: string, name: string): string;
|
|
8
|
+
export interface SpecRequirement {
|
|
9
|
+
id: string;
|
|
10
|
+
capability: string;
|
|
11
|
+
name: string;
|
|
12
|
+
file: string;
|
|
13
|
+
line: number;
|
|
14
|
+
}
|
|
15
|
+
export interface TraceTest {
|
|
16
|
+
repositoryId: string;
|
|
17
|
+
file: string;
|
|
18
|
+
line: number;
|
|
19
|
+
}
|
|
20
|
+
export interface TraceTag extends TraceTest {
|
|
21
|
+
tag: string;
|
|
22
|
+
form: 'title' | 'comment';
|
|
23
|
+
}
|
|
24
|
+
export interface TraceRepository {
|
|
25
|
+
id: string;
|
|
26
|
+
root: string;
|
|
27
|
+
}
|
|
28
|
+
export interface TraceCommand {
|
|
29
|
+
repositoryId: string;
|
|
30
|
+
command: string;
|
|
31
|
+
args: string[];
|
|
32
|
+
cwd?: string;
|
|
33
|
+
exitCode: number | null;
|
|
34
|
+
}
|
|
35
|
+
export type ReachesTest = (root: string, command: {
|
|
36
|
+
command: string;
|
|
37
|
+
args: string[];
|
|
38
|
+
cwd?: string;
|
|
39
|
+
}, file: string) => boolean;
|
|
40
|
+
export interface SpecTraceInput {
|
|
41
|
+
/** The checkout that holds `openspec/` (the run's artifact root). */
|
|
42
|
+
specsRoot: string;
|
|
43
|
+
/** Changes whose ADDED/MODIFIED/RENAMED requirements are "touched" (and checked by `--check`). */
|
|
44
|
+
changes?: string[];
|
|
45
|
+
repositories: TraceRepository[];
|
|
46
|
+
/** Explicit test files (the verification plan's or the developer's), repository-relative; added to discovery. */
|
|
47
|
+
testFiles?: Array<{
|
|
48
|
+
repositoryId: string;
|
|
49
|
+
file: string;
|
|
50
|
+
}>;
|
|
51
|
+
/** Restrict discovery to these repository-relative globs (`src/**\/*.test.ts`); default: every test-shaped file. */
|
|
52
|
+
testGlobs?: string[];
|
|
53
|
+
/** Verification evidence: the receipt Core recorded. */
|
|
54
|
+
receipt?: {
|
|
55
|
+
id?: string;
|
|
56
|
+
commands: TraceCommand[];
|
|
57
|
+
} | null;
|
|
58
|
+
/** No receipt, but the caller already ran the tests successfully (a CI step after `npm test`). */
|
|
59
|
+
testsPassed?: boolean;
|
|
60
|
+
/** Whether a command executes a test file; Core injects its full reachability check. Default: literal argument inspection. */
|
|
61
|
+
reaches?: ReachesTest;
|
|
62
|
+
now?: () => Date;
|
|
63
|
+
}
|
|
64
|
+
export type UndemonstratedReason = 'no-test' | 'not-executed' | 'failed';
|
|
65
|
+
export interface SpecTraceReport {
|
|
66
|
+
schemaVersion: typeof SPEC_TRACE_SCHEMA_VERSION;
|
|
67
|
+
generatedAt: string;
|
|
68
|
+
changes: string[];
|
|
69
|
+
/** `receipt`: executed means reached by a passing receipt command; `tests-passed`: tagged files count as executed; `none`: nothing counts as executed. */
|
|
70
|
+
evidence: 'receipt' | 'tests-passed' | 'none';
|
|
71
|
+
receiptId: string | null;
|
|
72
|
+
requirements: number;
|
|
73
|
+
demonstrated: Array<{
|
|
74
|
+
id: string;
|
|
75
|
+
name: string;
|
|
76
|
+
tests: TraceTest[];
|
|
77
|
+
}>;
|
|
78
|
+
undemonstrated: Array<{
|
|
79
|
+
id: string;
|
|
80
|
+
name: string;
|
|
81
|
+
reason: UndemonstratedReason;
|
|
82
|
+
tests: TraceTest[];
|
|
83
|
+
}>;
|
|
84
|
+
/** Tags that match no requirement (a typo, a renamed or removed requirement). Advisory. */
|
|
85
|
+
untagged: TraceTag[];
|
|
86
|
+
/** Requirements the changes add, modify or rename. */
|
|
87
|
+
touched: string[];
|
|
88
|
+
/** Touched requirements that are not demonstrated: what `--check` fails on. */
|
|
89
|
+
touchedUndemonstrated: string[];
|
|
90
|
+
scannedFiles: number;
|
|
91
|
+
}
|
|
92
|
+
/** Requirements of the canonical specs, in capability then file order. */
|
|
93
|
+
export declare function readSpecRequirements(specsRoot: string): SpecRequirement[];
|
|
94
|
+
export interface ChangeDelta {
|
|
95
|
+
added: SpecRequirement[];
|
|
96
|
+
modified: SpecRequirement[];
|
|
97
|
+
removed: string[];
|
|
98
|
+
renamed: Array<{
|
|
99
|
+
from: string;
|
|
100
|
+
to: SpecRequirement;
|
|
101
|
+
}>;
|
|
102
|
+
}
|
|
103
|
+
/** The change's directory: active, or its most recent archive (`archive/YYYY-MM-DD-<change>`). */
|
|
104
|
+
export declare function changeDirectory(specsRoot: string, change: string): string | undefined;
|
|
105
|
+
/** ADDED / MODIFIED / REMOVED / RENAMED requirements of a change's delta specs. */
|
|
106
|
+
export declare function readChangeDelta(specsRoot: string, change: string): ChangeDelta;
|
|
107
|
+
/** Active (unarchived) changes, for a CI check that names none. */
|
|
108
|
+
export declare function activeChanges(specsRoot: string): string[];
|
|
109
|
+
/** Tags in a test file's text with their 1-based lines. */
|
|
110
|
+
export declare function scanTags(text: string): Array<{
|
|
111
|
+
tag: string;
|
|
112
|
+
line: number;
|
|
113
|
+
form: 'title' | 'comment';
|
|
114
|
+
}>;
|
|
115
|
+
/** Literal reachability: a command naming test-shaped paths reaches only those; any other command is assumed to discover every test. */
|
|
116
|
+
export declare const literalReaches: ReachesTest;
|
|
117
|
+
/** The traceability report for the specs and tests under the input roots. Never throws for unreadable files; they are skipped. */
|
|
118
|
+
export declare function traceSpecs(input: SpecTraceInput): SpecTraceReport;
|
|
119
|
+
/** Requirement ids a change adds, modifies or renames: the ones its developer tags and `--check` enforces. */
|
|
120
|
+
export declare function touchedRequirements(specsRoot: string, change: string): string[];
|
|
121
|
+
/** One-line summary shown to hosts and reviewers: `Requirements demonstrated X/Y`. */
|
|
122
|
+
export declare function traceSummary(report: Pick<SpecTraceReport, 'demonstrated' | 'requirements'>): string;
|
|
123
|
+
export declare function renderTraceReport(report: SpecTraceReport): string;
|
|
124
|
+
/**
|
|
125
|
+
* `pipeline trace [--root <dir>] [--context <context.json>] [--change <name>[,…]] [--tests <glob>[,…]] [--tests-passed] [--check] [--json] [--out <file>]`.
|
|
126
|
+
* Normal mode is fail-open: it reports and exits 0 even when it cannot read
|
|
127
|
+
* something. `--check` exits 1 when a requirement touched by the change is
|
|
128
|
+
* not demonstrated (or the scan itself fails).
|
|
129
|
+
*/
|
|
130
|
+
export declare function runSpecTraceCli(argv: string[], options?: {
|
|
131
|
+
reaches?: ReachesTest;
|
|
132
|
+
stdout?: (text: string) => void;
|
|
133
|
+
stderr?: (text: string) => void;
|
|
134
|
+
}): Promise<number>;
|