spexcode 0.6.7 → 0.7.0-next.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/README.md +12 -7
- package/node_modules/@spexcode/session-application/dist/index.d.ts +17 -0
- package/node_modules/@spexcode/session-application/dist/index.js +35 -0
- package/node_modules/@spexcode/session-application/dist/migration.d.ts +51 -0
- package/node_modules/@spexcode/session-application/dist/migration.js +694 -0
- package/node_modules/@spexcode/session-application/dist/production.d.ts +109 -0
- package/node_modules/@spexcode/session-application/dist/production.js +438 -0
- package/node_modules/@spexcode/session-application/dist/schema.d.ts +2 -0
- package/node_modules/@spexcode/session-application/dist/schema.js +39 -0
- package/node_modules/@spexcode/session-application/package.json +29 -0
- package/node_modules/@spexcode/session-events/dist/errors.d.ts +6 -0
- package/node_modules/@spexcode/session-events/dist/errors.js +11 -0
- package/node_modules/@spexcode/session-events/dist/index.d.ts +46 -0
- package/node_modules/@spexcode/session-events/dist/index.js +245 -0
- package/node_modules/@spexcode/session-events/dist/schema.d.ts +3 -0
- package/node_modules/@spexcode/session-events/dist/schema.js +44 -0
- package/node_modules/@spexcode/session-events/package.json +24 -0
- package/node_modules/@spexcode/session-protocol/dist/canonical.d.ts +36 -0
- package/node_modules/@spexcode/session-protocol/dist/canonical.js +139 -0
- package/node_modules/@spexcode/session-protocol/dist/engine.d.ts +22 -0
- package/node_modules/@spexcode/session-protocol/dist/engine.js +478 -0
- package/node_modules/@spexcode/session-protocol/dist/errors.d.ts +8 -0
- package/node_modules/@spexcode/session-protocol/dist/errors.js +39 -0
- package/node_modules/@spexcode/session-protocol/dist/index.d.ts +60 -0
- package/node_modules/@spexcode/session-protocol/dist/index.js +4 -0
- package/node_modules/@spexcode/session-protocol/dist/schema.d.ts +36 -0
- package/node_modules/@spexcode/session-protocol/dist/schema.js +192 -0
- package/node_modules/@spexcode/{session-core → session-protocol}/package.json +4 -8
- package/node_modules/@spexcode/session-runtime/dist/errors.d.ts +6 -0
- package/node_modules/@spexcode/session-runtime/dist/errors.js +11 -0
- package/node_modules/@spexcode/session-runtime/dist/index.d.ts +33 -0
- package/node_modules/@spexcode/session-runtime/dist/index.js +193 -0
- package/node_modules/@spexcode/session-runtime/dist/schema.d.ts +2 -0
- package/node_modules/@spexcode/session-runtime/dist/schema.js +37 -0
- package/node_modules/@spexcode/session-runtime/package.json +24 -0
- package/node_modules/@spexcode/session-selflaunch/bin/spex-session.mjs +4 -0
- package/node_modules/@spexcode/session-selflaunch/dist/cli.d.ts +25 -0
- package/node_modules/@spexcode/session-selflaunch/dist/cli.js +156 -0
- package/node_modules/@spexcode/session-selflaunch/dist/index.d.ts +28 -0
- package/node_modules/@spexcode/session-selflaunch/dist/index.js +25 -0
- package/node_modules/@spexcode/session-selflaunch/dist/locality.d.ts +31 -0
- package/node_modules/@spexcode/session-selflaunch/dist/locality.js +135 -0
- package/node_modules/@spexcode/session-selflaunch/dist/path.d.ts +13 -0
- package/node_modules/@spexcode/session-selflaunch/dist/path.js +45 -0
- package/node_modules/@spexcode/session-selflaunch/package.json +35 -0
- package/node_modules/@spexcode/session-topology/dist/errors.d.ts +6 -0
- package/node_modules/@spexcode/session-topology/dist/errors.js +11 -0
- package/node_modules/@spexcode/session-topology/dist/index.d.ts +24 -0
- package/node_modules/@spexcode/session-topology/dist/index.js +173 -0
- package/node_modules/@spexcode/session-topology/dist/schema.d.ts +3 -0
- package/node_modules/@spexcode/session-topology/dist/schema.js +27 -0
- package/node_modules/@spexcode/session-topology/package.json +24 -0
- package/node_modules/@spexcode/spec-cli/bin/spex.mjs +175 -21
- package/node_modules/@spexcode/spec-cli/dist/cli.js +121 -59
- package/node_modules/@spexcode/spec-cli/dist/client.d.ts +1 -3
- package/node_modules/@spexcode/spec-cli/dist/client.js +49 -30
- package/node_modules/@spexcode/spec-cli/dist/codex-runtime-generations.d.ts +11 -0
- package/node_modules/@spexcode/spec-cli/dist/codex-runtime-generations.js +46 -9
- package/node_modules/@spexcode/spec-cli/dist/delivery-lock.d.ts +2 -0
- package/node_modules/@spexcode/spec-cli/dist/delivery-lock.js +58 -0
- package/node_modules/@spexcode/spec-cli/dist/doctor.js +53 -11
- package/node_modules/@spexcode/spec-cli/dist/file-write.js +14 -2
- package/node_modules/@spexcode/spec-cli/dist/gateway-hub.js +2 -1
- package/node_modules/@spexcode/spec-cli/dist/gateway.js +6 -3
- package/node_modules/@spexcode/spec-cli/dist/graphCache.js +32 -2
- package/node_modules/@spexcode/spec-cli/dist/graphSnapshot.js +57 -2
- package/node_modules/@spexcode/spec-cli/dist/graphStream.d.ts +2 -0
- package/node_modules/@spexcode/spec-cli/dist/graphStream.js +83 -3
- package/node_modules/@spexcode/spec-cli/dist/guide.js +29 -18
- package/node_modules/@spexcode/spec-cli/dist/harness-select.js +16 -3
- package/node_modules/@spexcode/spec-cli/dist/harness.d.ts +24 -6
- package/node_modules/@spexcode/spec-cli/dist/harness.js +510 -72
- package/node_modules/@spexcode/spec-cli/dist/help.js +11 -8
- package/node_modules/@spexcode/spec-cli/dist/hook-prompts.js +8 -0
- package/node_modules/@spexcode/spec-cli/dist/host-resources.js +29 -8
- package/node_modules/@spexcode/spec-cli/dist/host.d.ts +7 -0
- package/node_modules/@spexcode/spec-cli/dist/host.js +98 -1
- package/node_modules/@spexcode/spec-cli/dist/index.js +294 -36
- package/node_modules/@spexcode/spec-cli/dist/init.js +1 -1
- package/node_modules/@spexcode/spec-cli/dist/lint.js +70 -35
- package/node_modules/@spexcode/spec-cli/dist/listen.d.ts +3 -2
- package/node_modules/@spexcode/spec-cli/dist/listen.js +14 -2
- package/node_modules/@spexcode/spec-cli/dist/machine-peer.d.ts +2 -1
- package/node_modules/@spexcode/spec-cli/dist/machine-peer.js +46 -15
- package/node_modules/@spexcode/spec-cli/dist/materialize.d.ts +2 -2
- package/node_modules/@spexcode/spec-cli/dist/materialize.js +176 -35
- package/node_modules/@spexcode/spec-cli/dist/opencode.js +10 -2
- package/node_modules/@spexcode/spec-cli/dist/pi-harness.js +18 -5
- package/node_modules/@spexcode/spec-cli/dist/pi-headless.d.ts +2 -0
- package/node_modules/@spexcode/spec-cli/dist/pi-headless.js +27 -0
- package/node_modules/@spexcode/spec-cli/dist/pty-bridge.js +14 -14
- package/node_modules/@spexcode/spec-cli/dist/reviews.js +12 -7
- package/node_modules/@spexcode/spec-cli/dist/runtime-ownership.d.ts +11 -0
- package/node_modules/@spexcode/spec-cli/dist/runtime-ownership.js +79 -1
- package/node_modules/@spexcode/spec-cli/dist/session-application.d.ts +23 -0
- package/node_modules/@spexcode/spec-cli/dist/session-application.js +189 -0
- package/node_modules/@spexcode/spec-cli/dist/session-declarations.js +13 -1
- package/node_modules/@spexcode/spec-cli/dist/session-files.d.ts +6 -0
- package/node_modules/@spexcode/spec-cli/dist/session-files.js +13 -1
- package/node_modules/@spexcode/spec-cli/dist/session-follow.js +39 -22
- package/node_modules/@spexcode/{session-core/dist/record-lock.d.ts → spec-cli/dist/session-record-lock.d.ts} +0 -4
- package/node_modules/@spexcode/spec-cli/dist/session-record-lock.js +94 -0
- package/node_modules/@spexcode/spec-cli/dist/session-runtime-adapter.d.ts +44 -0
- package/node_modules/@spexcode/spec-cli/dist/session-runtime-adapter.js +37 -0
- package/node_modules/@spexcode/spec-cli/dist/session-timeline.d.ts +25 -2
- package/node_modules/@spexcode/spec-cli/dist/session-timeline.js +68 -11
- package/node_modules/@spexcode/spec-cli/dist/session-transcript.d.ts +57 -0
- package/node_modules/@spexcode/spec-cli/dist/session-transcript.js +137 -0
- package/node_modules/@spexcode/spec-cli/dist/session-web.js +4 -4
- package/node_modules/@spexcode/spec-cli/dist/sessions.d.ts +112 -15
- package/node_modules/@spexcode/spec-cli/dist/sessions.js +1553 -764
- package/node_modules/@spexcode/spec-cli/dist/shim-runtime.js +14 -0
- package/node_modules/@spexcode/spec-cli/dist/source-list.d.ts +13 -0
- package/node_modules/@spexcode/spec-cli/dist/source-list.js +99 -0
- package/node_modules/@spexcode/spec-cli/dist/source-read.d.ts +16 -0
- package/node_modules/@spexcode/spec-cli/dist/source-read.js +84 -0
- package/node_modules/@spexcode/spec-cli/dist/spec-attachments.d.ts +7 -0
- package/node_modules/@spexcode/spec-cli/dist/spec-attachments.js +89 -0
- package/node_modules/@spexcode/spec-cli/dist/spec-body-edit.d.ts +23 -0
- package/node_modules/@spexcode/spec-cli/dist/spec-body-edit.js +138 -0
- package/node_modules/@spexcode/spec-cli/dist/supervise.js +15 -6
- package/node_modules/@spexcode/spec-cli/hooks/dispatch.sh +19 -31
- package/node_modules/@spexcode/spec-cli/hooks/harness.sh +6 -6
- package/node_modules/@spexcode/spec-cli/package.json +8 -6
- package/node_modules/@spexcode/spec-cli/templates/hooks/post-checkout +4 -2
- package/node_modules/@spexcode/spec-cli/templates/hooks/post-merge +2 -1
- package/node_modules/@spexcode/spec-cli/templates/hooks/pre-commit +5 -3
- package/node_modules/@spexcode/spec-cli/templates/hooks/reference-transaction +5 -3
- package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/commands/spec.md +2 -7
- package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/idle/idle.sh +4 -10
- package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/idle/spec.md +1 -1
- package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/mark-active/mark-active.sh +22 -24
- package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/mark-active/spec.md +10 -2
- package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/session-fail/fail.sh +8 -7
- package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/session-fail/spec.md +3 -1
- package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/session-listen/session-listen.sh +133 -0
- package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/session-listen/spec.md +36 -0
- package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/spec.md +2 -0
- package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/stop-gate/spec.md +1 -1
- package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/stop-gate/stop-gate.sh +17 -20
- package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/skills/merge/spec.md +33 -0
- package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/skills/spec.md +2 -6
- package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/spec.md +7 -0
- package/node_modules/@spexcode/spec-core/dist/anchors.d.ts +13 -2
- package/node_modules/@spexcode/spec-core/dist/anchors.js +311 -15
- package/node_modules/@spexcode/spec-core/dist/git.d.ts +4 -0
- package/node_modules/@spexcode/spec-core/dist/git.js +9 -9
- package/node_modules/@spexcode/spec-core/dist/layout.d.ts +12 -1
- package/node_modules/@spexcode/spec-core/dist/layout.js +4 -2
- package/node_modules/@spexcode/spec-core/dist/resilience.d.ts +1 -0
- package/node_modules/@spexcode/spec-core/dist/resilience.js +8 -0
- package/node_modules/@spexcode/spec-core/dist/review/reviewFilters.d.ts +1 -0
- package/node_modules/@spexcode/spec-core/dist/review/reviewFilters.js +7 -3
- package/node_modules/@spexcode/spec-core/dist/review/reviewQuery.d.ts +1 -0
- package/node_modules/@spexcode/spec-core/dist/review/reviewQuery.js +4 -0
- package/node_modules/@spexcode/spec-core/dist/specs.d.ts +2 -0
- package/node_modules/@spexcode/spec-core/dist/specs.js +30 -0
- package/node_modules/@spexcode/spec-core/package.json +4 -1
- package/node_modules/@spexcode/spec-core/templates/spexcode.json +36 -13
- package/node_modules/@spexcode/spec-eval/dist/cli.js +1 -1
- package/node_modules/@spexcode/spec-eval/dist/evaltab.d.ts +1 -0
- package/node_modules/@spexcode/spec-eval/dist/evaltab.js +6 -1
- package/node_modules/@spexcode/spec-eval/dist/freshness.d.ts +1 -1
- package/node_modules/@spexcode/spec-eval/dist/freshness.js +12 -12
- package/node_modules/@spexcode/spec-eval/dist/host.d.ts +2 -1
- package/node_modules/@spexcode/spec-eval/dist/sessioneval.d.ts +2 -1
- package/node_modules/@spexcode/spec-eval/dist/sessioneval.js +20 -3
- package/node_modules/@spexcode/spec-eval/package.json +2 -2
- package/node_modules/@spexcode/spec-forge/package.json +2 -2
- package/node_modules/@spexcode/transcript/dist/frames.d.ts +44 -0
- package/node_modules/@spexcode/transcript/dist/frames.js +85 -0
- package/node_modules/@spexcode/transcript/dist/index.d.ts +5 -0
- package/node_modules/@spexcode/transcript/dist/index.js +5 -0
- package/node_modules/@spexcode/transcript/dist/live.d.ts +16 -0
- package/node_modules/@spexcode/transcript/dist/live.js +65 -0
- package/node_modules/@spexcode/transcript/dist/parsers.d.ts +47 -0
- package/node_modules/@spexcode/transcript/dist/parsers.js +285 -0
- package/node_modules/@spexcode/transcript/dist/readers.d.ts +10 -0
- package/node_modules/@spexcode/transcript/dist/readers.js +296 -0
- package/node_modules/@spexcode/transcript/dist/turns.d.ts +42 -0
- package/node_modules/@spexcode/transcript/dist/turns.js +11 -0
- package/node_modules/@spexcode/transcript/package.json +30 -0
- package/node_modules/@vscode/tree-sitter-wasm/LICENSE +21 -0
- package/node_modules/@vscode/tree-sitter-wasm/README.md +36 -0
- package/node_modules/@vscode/tree-sitter-wasm/SECURITY.md +41 -0
- package/node_modules/@vscode/tree-sitter-wasm/cgmanifest.json +16 -0
- package/node_modules/@vscode/tree-sitter-wasm/package.json +42 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-bash.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-c-sharp.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-cpp.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-css.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-go.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-ini.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-java.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-javascript.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-php.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-powershell.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-python.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-regex.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-ruby.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-rust.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-tsx.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-typescript.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter.js +4075 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/web-tree-sitter.d.ts +1027 -0
- package/node_modules/smol-toml/LICENSE +24 -0
- package/node_modules/smol-toml/README.md +418 -0
- package/node_modules/smol-toml/dist/date.d.ts +41 -0
- package/node_modules/smol-toml/dist/date.js +127 -0
- package/node_modules/smol-toml/dist/error.d.ts +38 -0
- package/node_modules/smol-toml/dist/error.js +63 -0
- package/node_modules/smol-toml/dist/extract.js +69 -0
- package/node_modules/smol-toml/dist/index.cjs +734 -0
- package/node_modules/smol-toml/dist/index.d.ts +43 -0
- package/node_modules/smol-toml/dist/index.js +33 -0
- package/node_modules/smol-toml/dist/parse.d.ts +36 -0
- package/node_modules/smol-toml/dist/parse.js +149 -0
- package/node_modules/smol-toml/dist/primitive.js +238 -0
- package/node_modules/smol-toml/dist/stringify.d.ts +31 -0
- package/node_modules/smol-toml/dist/stringify.js +181 -0
- package/node_modules/smol-toml/dist/struct.js +179 -0
- package/node_modules/smol-toml/dist/util.d.ts +38 -0
- package/node_modules/smol-toml/dist/util.js +89 -0
- package/node_modules/smol-toml/package.json +68 -0
- package/package.json +9 -3
- package/node_modules/@spexcode/session-core/dist/delivery-queue.d.ts +0 -24
- package/node_modules/@spexcode/session-core/dist/delivery-queue.js +0 -206
- package/node_modules/@spexcode/session-core/dist/index.d.ts +0 -5
- package/node_modules/@spexcode/session-core/dist/index.js +0 -5
- package/node_modules/@spexcode/session-core/dist/internal.d.ts +0 -3
- package/node_modules/@spexcode/session-core/dist/internal.js +0 -3
- package/node_modules/@spexcode/session-core/dist/message.d.ts +0 -22
- package/node_modules/@spexcode/session-core/dist/message.js +0 -53
- package/node_modules/@spexcode/session-core/dist/record-lock.js +0 -152
- package/node_modules/@spexcode/session-core/dist/runtime-session.d.ts +0 -62
- package/node_modules/@spexcode/session-core/dist/runtime-session.js +0 -326
- package/node_modules/@spexcode/session-core/dist/session-cursors.d.ts +0 -14
- package/node_modules/@spexcode/session-core/dist/session-cursors.js +0 -82
- package/node_modules/@spexcode/session-core/dist/session-timeline.d.ts +0 -47
- package/node_modules/@spexcode/session-core/dist/session-timeline.js +0 -216
- package/node_modules/@spexcode/spec-cli/dist/execution-trace.d.ts +0 -26
- package/node_modules/@spexcode/spec-cli/dist/execution-trace.js +0 -475
- package/node_modules/@spexcode/spec-cli/dist/session-execution.d.ts +0 -10
- package/node_modules/@spexcode/spec-cli/dist/session-execution.js +0 -70
- package/node_modules/@spexcode/spec-cli/hooks/compat/mark-active-0.5.2-eef1.fixture +0 -53
- package/node_modules/@spexcode/spec-cli/hooks/compat/mark-active-sed-v0.fixture +0 -46
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { readFileSync, existsSync, statSync } from 'node:fs';
|
|
2
|
-
import { join } from 'node:path';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
3
|
import { repoRoot, git, sourceIndexes, rowsFor, treeFilePaths, treeFileText, withEventLedgerBuild } from '@spexcode/spec-core';
|
|
4
|
-
import { loadSpecs, parseFrontmatter } from '@spexcode/spec-core';
|
|
4
|
+
import { bodyMentions, loadSpecs, parseFrontmatter } from '@spexcode/spec-core';
|
|
5
5
|
import { readJsonConfig } from '@spexcode/spec-core';
|
|
6
|
-
import { extractors, extractorFor, extOf, parseCodeEntry, relationClaimsPath, resolveSelectors, windowEvents, anchorHitQueries } from '@spexcode/spec-core';
|
|
6
|
+
import { extractors, extractorFor, extOf, parseCodeEntry, parseRelation, relationClaimsPath, resolveSelectors, windowEvents, anchorHitQueries } from '@spexcode/spec-core';
|
|
7
|
+
import { EVAL_FILE, parseScenarios } from '@spexcode/spec-eval/scenarios';
|
|
7
8
|
import { DEFAULT_TEST_GLOBS, sourcePolicyDescription, trackedSourceFiles } from './source-files.js';
|
|
8
9
|
export const SPEC_LINT_REPORT_PROJECTION = 'spex.spec-lint.report';
|
|
9
10
|
export const SPEC_LINT_REPORT_SCHEMA_VERSION = 1;
|
|
@@ -276,24 +277,17 @@ async function specLintInLedger(root, regs, options) {
|
|
|
276
277
|
}
|
|
277
278
|
// mention: a `[[id]]` in a body must name a real node (ERROR — a dangling mention is a broken edge in
|
|
278
279
|
// the very graph the tree exists to keep honest). Checked against the SAME minted ids every other
|
|
279
|
-
// surface resolves ([[id-url-safe]]).
|
|
280
|
-
//
|
|
280
|
+
// surface resolves ([[id-url-safe]]). The body reader is `bodyMentions`, and this rule is now its ONLY
|
|
281
|
+
// consumer: the loader used to ship the surviving names as a per-node edge, and that projection is gone
|
|
282
|
+
// ([[context-dock]]) because a prose mention is a fact about the graph, never about the node the reader
|
|
283
|
+
// has open. The parser stays where it is — judging what is prose and what is sample text is the same
|
|
284
|
+
// judgement whoever asks, and this rule is the one place a name must be resolved at all.
|
|
285
|
+
// One finding per distinct dangling name: a name repeated in one body is still one broken edge.
|
|
281
286
|
const idSet = new Set(specs.map((s) => s.id));
|
|
282
|
-
const MENTION_RE = /\[\[(\.?[\p{L}\p{N}_-]+)\]\]/gu;
|
|
283
287
|
for (const s of specs) {
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
inFence = !inFence;
|
|
288
|
-
continue;
|
|
289
|
-
}
|
|
290
|
-
if (inFence)
|
|
291
|
-
continue;
|
|
292
|
-
const line = rawLine.replace(/`[^`]*`/g, '');
|
|
293
|
-
for (const m of line.matchAll(MENTION_RE)) {
|
|
294
|
-
if (!idSet.has(m[1]))
|
|
295
|
-
out.push({ level: 'error', rule: 'mention', spec: s.id, msg: `'${s.id}' mentions [[${m[1]}]] — no such node; retarget or drop it (backtick it if it is sample text)` });
|
|
296
|
-
}
|
|
288
|
+
for (const id of bodyMentions(s.body)) {
|
|
289
|
+
if (!idSet.has(id))
|
|
290
|
+
out.push({ level: 'error', rule: 'mention', spec: s.id, msg: `'${s.id}' mentions [[${id}]] — no such node; retarget or drop it (backtick it if it is sample text)` });
|
|
297
291
|
}
|
|
298
292
|
}
|
|
299
293
|
// living: a spec body describes the node's CURRENT intent — it is not a changelog. Version history
|
|
@@ -329,19 +323,53 @@ async function specLintInLedger(root, regs, options) {
|
|
|
329
323
|
const top = over.slice(0, 5).map(([f, ids]) => `${f.split('/').pop()}(${ids.length})`).join(', ');
|
|
330
324
|
out.push({ level: 'warn', rule: 'owners', msg: `${over.length} file(s) are governed by > ${cfg.maxOwners} nodes — each holds more separately-specified functionality than one file should. Worst: ${top}. SPLIT the file so each governor owns its own module (or merge the nodes, or give it a single foundation owner + related:).` });
|
|
331
325
|
}
|
|
326
|
+
const nodeSource = (s, relation, entries) => ({ relation, entries, drift: true, owner: `'${s.id}'`, repair: `the spec's ${relation}: entry` });
|
|
327
|
+
const scenarioSources = (s) => {
|
|
328
|
+
const evalPath = join(dirname(s.path), EVAL_FILE);
|
|
329
|
+
if (!existsAtTip(evalPath))
|
|
330
|
+
return [];
|
|
331
|
+
let scenarios;
|
|
332
|
+
// A malformed eval.md is `spex eval lint`'s eval-schema finding, not this gate's: refusing to parse it
|
|
333
|
+
// here would turn one measurement-layer typo into a blocked commit.
|
|
334
|
+
try {
|
|
335
|
+
scenarios = parseScenarios(textAtTip(evalPath) ?? '');
|
|
336
|
+
}
|
|
337
|
+
catch {
|
|
338
|
+
return [];
|
|
339
|
+
}
|
|
340
|
+
return scenarios.flatMap((sc) => [
|
|
341
|
+
// ONLY the scenario's OWN declarations. An empty scenario `code:` inherits the node's axis, and those
|
|
342
|
+
// entries are already this node's source above — re-reading them here would double-report one selector.
|
|
343
|
+
{ relation: 'code', raw: sc.code },
|
|
344
|
+
{ relation: 'related', raw: sc.related },
|
|
345
|
+
].flatMap(({ relation, raw }) => raw?.length
|
|
346
|
+
? [{ relation, entries: parseRelation([...raw], relation).entries, drift: false,
|
|
347
|
+
owner: `'${s.id}' scenario '${sc.name}'`, repair: `the scenario's ${relation}: entry in ${evalPath}` }]
|
|
348
|
+
: []));
|
|
349
|
+
};
|
|
332
350
|
const readyWarned = new Set();
|
|
333
351
|
const anchorSteps = [];
|
|
352
|
+
// One parse per candidate file per run, shared by every source that anchors it. Without it a file many
|
|
353
|
+
// scenarios anchor is re-extracted once per scenario, and extraction is the expensive half of this gate.
|
|
354
|
+
const unitsAtTip = new Map();
|
|
334
355
|
for (const s of specs) {
|
|
335
|
-
for (const
|
|
336
|
-
|
|
356
|
+
for (const src of [
|
|
357
|
+
nodeSource(s, 'code', s.codeScoped),
|
|
358
|
+
nodeSource(s, 'related', s.relatedScoped),
|
|
359
|
+
...scenarioSources(s),
|
|
360
|
+
]) {
|
|
361
|
+
const { relation, drift, owner, repair } = src;
|
|
362
|
+
for (const { path, selectors } of src.entries) {
|
|
363
|
+
if (!selectors.length)
|
|
364
|
+
continue;
|
|
337
365
|
if (pending && !changed.some((file) => relationClaimsPath(path, file)))
|
|
338
366
|
continue;
|
|
339
367
|
const x = extractorFor(regs, extOf(path));
|
|
340
368
|
if (!x) {
|
|
341
|
-
anchorSteps.push({ finding: { level: 'error', rule: 'integrity', spec: s.id, file: path, msg:
|
|
369
|
+
anchorSteps.push({ finding: { level: 'error', rule: 'integrity', spec: s.id, file: path, msg: `${owner} anchors ${path}#${selectors.join(', #')} (${relation}:), but no extractor is designated for '.${extOf(path)}' files — anchor validation was skipped and remains unverified; add a Tree-sitter language row (anchors.ts) or drop the selector(s)` } });
|
|
342
370
|
continue;
|
|
343
371
|
}
|
|
344
|
-
const ready = x.ready();
|
|
372
|
+
const ready = await x.ready();
|
|
345
373
|
if (ready !== true) {
|
|
346
374
|
// once per (extractor, reason), even across several anchored nodes — one repair, one message.
|
|
347
375
|
if (!readyWarned.has(x.id + ready)) {
|
|
@@ -353,20 +381,27 @@ async function specLintInLedger(root, regs, options) {
|
|
|
353
381
|
if (!existsAtTip(path))
|
|
354
382
|
continue; // the missing FILE already errored above
|
|
355
383
|
if (isDirectoryAtTip(path)) {
|
|
356
|
-
anchorSteps.push({ finding: { level: 'error', rule: 'integrity', spec: s.id, file: path, msg:
|
|
384
|
+
anchorSteps.push({ finding: { level: 'error', rule: 'integrity', spec: s.id, file: path, msg: `${owner} puts a selector on a directory (${relation}: ${path}#${selectors[0]}) — a selector scopes ONE real file` } });
|
|
357
385
|
continue;
|
|
358
386
|
}
|
|
359
|
-
let
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
387
|
+
let cached = unitsAtTip.get(path);
|
|
388
|
+
if (!cached) {
|
|
389
|
+
try {
|
|
390
|
+
const source = textAtTip(path);
|
|
391
|
+
if (source === null)
|
|
392
|
+
throw new Error(`candidate tree has no file '${path}'`);
|
|
393
|
+
cached = { units: await x.extract(source, path) };
|
|
394
|
+
}
|
|
395
|
+
catch (e) {
|
|
396
|
+
cached = { error: e?.message ?? String(e) };
|
|
397
|
+
}
|
|
398
|
+
unitsAtTip.set(path, cached);
|
|
365
399
|
}
|
|
366
|
-
|
|
367
|
-
anchorSteps.push({ finding: { level: 'error', rule: 'integrity', spec: s.id, file: path, msg: `anchor ${path}#${selectors.join(', #')} (
|
|
400
|
+
if ('error' in cached) {
|
|
401
|
+
anchorSteps.push({ finding: { level: 'error', rule: 'integrity', spec: s.id, file: path, msg: `anchor ${path}#${selectors.join(', #')} (${owner}) is unverifiable — the current file does not parse: ${cached.error}` } });
|
|
368
402
|
continue;
|
|
369
403
|
}
|
|
404
|
+
const units = cached.units;
|
|
370
405
|
// each selector resolves (or errors) on its own; only the live ones feed the window engine.
|
|
371
406
|
// The dead/ambiguous verdict itself comes from the ONE shared classifier ([[code-anchor]]); only the
|
|
372
407
|
// wording of the gate's findings lives here.
|
|
@@ -374,18 +409,18 @@ async function specLintInLedger(root, regs, options) {
|
|
|
374
409
|
for (const res of resolveSelectors(units, selectors)) {
|
|
375
410
|
const sym = res.selector;
|
|
376
411
|
if ('dead' in res) {
|
|
377
|
-
anchorSteps.push({ finding: { level: 'error', rule: 'integrity', spec: s.id, file: path, msg: `dead anchor: ${path}#${sym} (
|
|
412
|
+
anchorSteps.push({ finding: { level: 'error', rule: 'integrity', spec: s.id, file: path, msg: `dead anchor: ${path}#${sym} (${owner}) names no unit on the current tree — the unit was deleted or renamed; update ${repair} to follow it` } });
|
|
378
413
|
continue;
|
|
379
414
|
}
|
|
380
415
|
if ('ambiguous' in res) {
|
|
381
|
-
anchorSteps.push({ finding: { level: 'error', rule: 'integrity', spec: s.id, file: path, msg: `ambiguous anchor: ${path}#${sym} (
|
|
416
|
+
anchorSteps.push({ finding: { level: 'error', rule: 'integrity', spec: s.id, file: path, msg: `ambiguous anchor: ${path}#${sym} (${owner}) names ${res.ambiguous} same-named units in one file — an anchor must be unique; rename one unit` } });
|
|
382
417
|
continue;
|
|
383
418
|
}
|
|
384
419
|
if (res.ok.typeOnly)
|
|
385
420
|
anchorSteps.push({ finding: { level: 'warn', rule: 'anchor', spec: s.id, file: path, msg: `${path}#${sym} anchors a ${res.ok.kind} — anchoring a type is usually wrong (types reshape with every refactor); anchor the behaviour-bearing unit instead` } });
|
|
386
421
|
live.push(sym);
|
|
387
422
|
}
|
|
388
|
-
if (!live.length)
|
|
423
|
+
if (!drift || !live.length)
|
|
389
424
|
continue;
|
|
390
425
|
const since = rowsFor(hidx, s.path)[0]?.hash || '';
|
|
391
426
|
const win = windowEvents(didx, since, path, s.id);
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { Server } from 'node:net';
|
|
2
|
+
export declare function resolveConfiguredPort(rawPort: string | undefined): number;
|
|
2
3
|
export declare function listenOrExit(server: Server, port: number, opts: {
|
|
3
4
|
host?: string;
|
|
4
5
|
label: string;
|
|
5
6
|
cleanup?: () => void;
|
|
6
|
-
onListen?: () => void;
|
|
7
|
-
ready: string | string[];
|
|
7
|
+
onListen?: (port: number) => void;
|
|
8
|
+
ready: string | string[] | ((port: number) => string | string[]);
|
|
8
9
|
}): void;
|
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
export function resolveConfiguredPort(rawPort) {
|
|
2
|
+
const normalized = rawPort?.trim();
|
|
3
|
+
if (!normalized)
|
|
4
|
+
return 8787;
|
|
5
|
+
const port = Number(normalized);
|
|
6
|
+
if (!Number.isInteger(port) || port < 0 || port > 65535) {
|
|
7
|
+
throw new Error(`PORT must be an integer from 0 to 65535, got ${JSON.stringify(rawPort)}`);
|
|
8
|
+
}
|
|
9
|
+
return port;
|
|
10
|
+
}
|
|
1
11
|
// @@@ listenOrExit ([[listener-readiness]]) - the one public-listener transition: before `listening`, a bind
|
|
2
12
|
// failure is loud and fatal; after it, publication side effects and user-visible ready lines may run. Keeping
|
|
3
13
|
// both halves here prevents a private child or pre-bind caller from announcing a surface it does not own.
|
|
@@ -13,8 +23,10 @@ export function listenOrExit(server, port, opts) {
|
|
|
13
23
|
process.exit(1);
|
|
14
24
|
});
|
|
15
25
|
const publishReady = () => {
|
|
16
|
-
|
|
17
|
-
|
|
26
|
+
const actualPort = server.address().port;
|
|
27
|
+
opts.onListen?.(actualPort);
|
|
28
|
+
const ready = typeof opts.ready === 'function' ? opts.ready(actualPort) : opts.ready;
|
|
29
|
+
for (const line of Array.isArray(ready) ? ready : [ready])
|
|
18
30
|
console.log(line);
|
|
19
31
|
};
|
|
20
32
|
if (opts.host)
|
|
@@ -51,8 +51,9 @@ export declare class MachinePeerGateway {
|
|
|
51
51
|
private control;
|
|
52
52
|
private retry;
|
|
53
53
|
private closing;
|
|
54
|
-
start(): void
|
|
54
|
+
start(): Promise<void>;
|
|
55
55
|
private startControl;
|
|
56
|
+
private listenControl;
|
|
56
57
|
private handleRpc;
|
|
57
58
|
private startInbound;
|
|
58
59
|
private stopInbound;
|
|
@@ -72,13 +72,17 @@ export function readPeerMachineId() {
|
|
|
72
72
|
writeStore({ ...store, machineId });
|
|
73
73
|
return machineId;
|
|
74
74
|
}
|
|
75
|
+
// @@@ peer sender spelling - the authenticated peer identity travels as the message's ordinary `senderSessionId`,
|
|
76
|
+
// so it must fit the protocol's frozen session_id grammar `[0-9A-Za-z_][0-9A-Za-z_-]*` (session-protocol §5.2:
|
|
77
|
+
// no `:`, no leading `-`; namespaces are encoded INTO the id). `peer_<machineId>_<sessionId>` is that encoding;
|
|
78
|
+
// the earlier `peer:` spelling was refused by every receiving backend with PROTOCOL_SESSION_ID_INVALID.
|
|
75
79
|
export function peerSenderRef(machineId, sessionId) {
|
|
76
|
-
return sessionId && validMachineId(sessionId) ? `
|
|
80
|
+
return sessionId && validMachineId(sessionId) ? `peer_${machineId}_${sessionId}` : `peer_${machineId}`;
|
|
77
81
|
}
|
|
78
82
|
function validPeerSender(value, machineId) {
|
|
79
83
|
if (typeof value !== 'string')
|
|
80
84
|
return false;
|
|
81
|
-
const match = value.match(/^
|
|
85
|
+
const match = value.match(/^peer_([0-9a-f-]{36})(?:_([0-9a-f-]{36}))?$/i);
|
|
82
86
|
return !!match && match[1] === machineId && (match[2] === undefined || validMachineId(match[2]));
|
|
83
87
|
}
|
|
84
88
|
export function findMachinePeer(sshAddress) {
|
|
@@ -160,7 +164,7 @@ function peerProjectsForSession(sessionId) {
|
|
|
160
164
|
const hits = [];
|
|
161
165
|
for (const id of ids) {
|
|
162
166
|
const root = join(projects, id);
|
|
163
|
-
const record = join(root, 'sessions', sessionId, '
|
|
167
|
+
const record = join(root, 'sessions', sessionId, 'runtime.json');
|
|
164
168
|
if (!existsSync(record))
|
|
165
169
|
continue;
|
|
166
170
|
const direct = readEndpointRecord(join(root, 'backend.json'));
|
|
@@ -367,34 +371,61 @@ function sshArgs(peer) {
|
|
|
367
371
|
'--', peer.sshAddress,
|
|
368
372
|
];
|
|
369
373
|
}
|
|
374
|
+
// true iff something ACCEPTS a connection at this unix path; any error (ECONNREFUSED for a leftover path, ENOENT
|
|
375
|
+
// for a vanished one) means no live listener.
|
|
376
|
+
function probeUnixListener(path) {
|
|
377
|
+
return new Promise((resolve) => {
|
|
378
|
+
const socket = createConnection(path);
|
|
379
|
+
socket.once('connect', () => { socket.destroy(); resolve(true); });
|
|
380
|
+
socket.once('error', () => resolve(false));
|
|
381
|
+
});
|
|
382
|
+
}
|
|
370
383
|
export class MachinePeerGateway {
|
|
371
384
|
inbound = new Map();
|
|
372
385
|
children = new Map();
|
|
373
386
|
control = null;
|
|
374
387
|
retry = null;
|
|
375
388
|
closing = false;
|
|
389
|
+
// start() resolves once the control socket is claimed. With no leftover path the claim is synchronous (the
|
|
390
|
+
// listener is issued before the promise settles, so callers may issue control RPCs right away); a leftover path
|
|
391
|
+
// is probed first, which is the only asynchronous branch.
|
|
376
392
|
start() {
|
|
377
393
|
for (const peer of listMachinePeers())
|
|
378
394
|
this.startInbound(peer);
|
|
379
|
-
this.startControl()
|
|
380
|
-
for (const peer of listMachinePeers())
|
|
381
|
-
if (peer.owner)
|
|
382
|
-
this.ensureTunnel(peer);
|
|
383
|
-
this.retry = setInterval(() => {
|
|
395
|
+
return this.startControl().then(() => {
|
|
384
396
|
for (const peer of listMachinePeers())
|
|
385
|
-
if (peer.owner
|
|
397
|
+
if (peer.owner)
|
|
386
398
|
this.ensureTunnel(peer);
|
|
387
|
-
|
|
388
|
-
|
|
399
|
+
this.retry = setInterval(() => {
|
|
400
|
+
for (const peer of listMachinePeers())
|
|
401
|
+
if (peer.owner && peer.state === 'connecting')
|
|
402
|
+
this.ensureTunnel(peer);
|
|
403
|
+
}, PEER_RETRY_MS);
|
|
404
|
+
this.retry.unref();
|
|
405
|
+
});
|
|
389
406
|
}
|
|
407
|
+
// @@@ stale control socket - a killed gateway never unlinks peer.sock, so the FILE proves nothing about
|
|
408
|
+
// ownership; only a connect does. A live gateway accepts the connection; a leftover path refuses it
|
|
409
|
+
// (ECONNREFUSED). So a missing path is claimed at once, a refusing path is unlinked and reclaimed, and only an
|
|
410
|
+
// accepting listener is "another `spex dashboard`" — the same test claude-rendezvous applies to its socket file.
|
|
390
411
|
startControl() {
|
|
391
412
|
const path = peerSocketPath();
|
|
392
413
|
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
393
|
-
if (existsSync(path)) {
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
throw error;
|
|
414
|
+
if (!existsSync(path)) {
|
|
415
|
+
this.listenControl(path);
|
|
416
|
+
return Promise.resolve();
|
|
397
417
|
}
|
|
418
|
+
return probeUnixListener(path).then((live) => {
|
|
419
|
+
if (live) {
|
|
420
|
+
const error = new Error(`machine peer service already owns ${path} — another \`spex dashboard\` is running`);
|
|
421
|
+
error.name = 'BackendError';
|
|
422
|
+
throw error;
|
|
423
|
+
}
|
|
424
|
+
rmSync(path, { force: true });
|
|
425
|
+
this.listenControl(path);
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
listenControl(path) {
|
|
398
429
|
this.control = createNetServer({ allowHalfOpen: true }, (socket) => {
|
|
399
430
|
let input = '';
|
|
400
431
|
socket.setEncoding('utf8');
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type HarnessArtifacts } from './harness.js';
|
|
1
|
+
import { GENERATED_MARK, type HarnessArtifacts } from './harness.js';
|
|
2
2
|
export type MaterializedArtifact = {
|
|
3
3
|
kind: 'hook manifest' | 'contract' | 'shim' | 'skill' | 'agent' | 'plugin bundle' | 'trust';
|
|
4
4
|
path: string;
|
|
@@ -13,6 +13,6 @@ export declare function retiredAxisNotice(cfg: {
|
|
|
13
13
|
private?: boolean;
|
|
14
14
|
}): void;
|
|
15
15
|
export declare function stripSpexcodeBlock(text: string, comment?: readonly [string, string]): string;
|
|
16
|
-
export
|
|
16
|
+
export { GENERATED_MARK };
|
|
17
17
|
export declare function dematerialize(proj?: string, arts?: HarnessArtifacts): void;
|
|
18
18
|
export declare function materialize(proj?: string): MaterializeResult;
|
|
@@ -4,13 +4,33 @@ import { fileURLToPath } from 'node:url';
|
|
|
4
4
|
import { execFileSync } from 'node:child_process';
|
|
5
5
|
import { loadSystemConfig, loadSkillConfig, loadAgentConfig, loadConfig } from '@spexcode/spec-core';
|
|
6
6
|
import { compileManifest } from './hooks.js';
|
|
7
|
-
import { writeManagedBlock, removeManagedBlock, HARNESSES } from './harness.js';
|
|
7
|
+
import { writeManagedBlock, removeManagedBlock, writeManagedJsonHooks, removeManagedJsonHooks, sharedShimHasHostContent, isGeneratedArtifact, GENERATED_MARK, HARNESSES } from './harness.js';
|
|
8
8
|
import { git, gitBinary } from '@spexcode/spec-core';
|
|
9
9
|
import { runtimeRoot, treeSlotDir, mainCheckout, readConfig } from '@spexcode/spec-core';
|
|
10
10
|
import { resolveHarnessTargets, partitionHarnesses } from './harness-select.js';
|
|
11
11
|
import { emitPlugin, cleanPlugin, pluginBundleDir, pluginVersion } from './plugin-harness.js';
|
|
12
12
|
import { clearContractFilterPayload, contractFilterPlanted, plantContractFilter, removeContractFilter, settleIndexStat } from './contract-filter.js';
|
|
13
13
|
import { writeFileIfChanged } from './file-write.js';
|
|
14
|
+
// land one shim. A file wholly ours is the plain byte-equality write; a config file the host agent SHARES with
|
|
15
|
+
// the user gets ONLY our identity-stamped hook entries merged in, so their permissions, env, statusLine and
|
|
16
|
+
// own hooks survive adoption (writeManagedJsonHooks, [[harness-adapter]]).
|
|
17
|
+
// A shared file we cannot PARSE is the one landing that can fail, and it fails for a reason that is the
|
|
18
|
+
// user's to fix. Report it and carry on: their broken JSON must not also cost them the contract, the skills
|
|
19
|
+
// and the allowlist this pass still owes every other target (the pre-commit anchor runs this on the way into
|
|
20
|
+
// every commit). Returns whether the shim actually landed.
|
|
21
|
+
function landShim(file, shim) {
|
|
22
|
+
try {
|
|
23
|
+
if (shim.ownership === 'shared-json' && shim.hooks)
|
|
24
|
+
writeManagedJsonHooks(file, shim.hooks);
|
|
25
|
+
else
|
|
26
|
+
writeFileIfChanged(file, shim.content);
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
catch (e) {
|
|
30
|
+
console.error(`spexcode: no hooks delivered to ${file} — ${e.message}`);
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
14
34
|
const PKG = fileURLToPath(new URL('..', import.meta.url)); // installed spec-cli root
|
|
15
35
|
const DISPATCH = join(PKG, 'hooks', 'dispatch.sh');
|
|
16
36
|
// the ONE spex entry: the launcher (bin/spex.mjs), never a raw source entry - the launcher runs compiled
|
|
@@ -50,6 +70,53 @@ export function retiredAxisNotice(cfg) {
|
|
|
50
70
|
function gitCommonDirOf(proj) {
|
|
51
71
|
return git(['-C', proj, 'rev-parse', '--path-format=absolute', '--git-common-dir']).trim();
|
|
52
72
|
}
|
|
73
|
+
// Codex resolves linked-worktree project hooks from the main checkout, but old materializers left a second
|
|
74
|
+
// executable copy in each worktree. Migrate only an exact SpexCode-only JSON config; a file with any user
|
|
75
|
+
// hook is theirs and remains untouched. This is a one-time identity migration, not a sibling configuration
|
|
76
|
+
// sweep: it never creates files and never changes the main checkout's owner.
|
|
77
|
+
function retireLegacyCodexAnchors(checkout) {
|
|
78
|
+
let listing;
|
|
79
|
+
try {
|
|
80
|
+
listing = git(['-C', checkout, 'worktree', 'list', '--porcelain']);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const paths = [...listing.matchAll(/^worktree (.+)$/gm)].map(match => match[1]).filter(path => path !== checkout);
|
|
86
|
+
for (const tree of paths) {
|
|
87
|
+
const file = join(tree, '.codex', 'hooks.json');
|
|
88
|
+
if (!existsSync(file))
|
|
89
|
+
continue;
|
|
90
|
+
let parsed;
|
|
91
|
+
try {
|
|
92
|
+
parsed = JSON.parse(readFileSync(file, 'utf8'));
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
98
|
+
continue;
|
|
99
|
+
const hooks = parsed.hooks;
|
|
100
|
+
if (!hooks || typeof hooks !== 'object' || Array.isArray(hooks))
|
|
101
|
+
continue;
|
|
102
|
+
const commands = [];
|
|
103
|
+
for (const groups of Object.values(hooks)) {
|
|
104
|
+
if (!Array.isArray(groups))
|
|
105
|
+
continue;
|
|
106
|
+
for (const group of groups) {
|
|
107
|
+
if (!group || typeof group !== 'object')
|
|
108
|
+
continue;
|
|
109
|
+
for (const hook of group.hooks ?? []) {
|
|
110
|
+
if (hook && typeof hook === 'object' && typeof hook.command === 'string')
|
|
111
|
+
commands.push(hook.command);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (!commands.length || commands.some(command => !command.includes('dispatch.sh')))
|
|
116
|
+
continue;
|
|
117
|
+
writeFileIfChanged(file, '{\n "hooks": {}\n}\n');
|
|
118
|
+
}
|
|
119
|
+
}
|
|
53
120
|
function infoExcludePath(proj) {
|
|
54
121
|
return join(gitCommonDirOf(proj), 'info', 'exclude');
|
|
55
122
|
}
|
|
@@ -112,8 +179,10 @@ function hostContentOf(file) {
|
|
|
112
179
|
return stripSpexcodeBlock(readFileSync(file, 'utf8'));
|
|
113
180
|
}
|
|
114
181
|
// the identity stamp on every generated skill/agent file — what lets the erase phase forget a product whose
|
|
115
|
-
// NODE was renamed or deleted (the name-scoped sweep can only reconstruct paths the LIVE config still names)
|
|
116
|
-
|
|
182
|
+
// NODE was renamed or deleted (the name-scoped sweep can only reconstruct paths the LIVE config still names),
|
|
183
|
+
// and what stops the write phase from landing on a same-named file the user wrote. Defined with the adapter
|
|
184
|
+
// (both halves of the pass need it); re-exported here for the commit-surgery reader.
|
|
185
|
+
export { GENERATED_MARK };
|
|
117
186
|
function pruneGeneratedSkills(dir, keep) {
|
|
118
187
|
if (!dir || !existsSync(dir))
|
|
119
188
|
return false;
|
|
@@ -176,11 +245,20 @@ function reconcileTree(proj, targets, tracked) {
|
|
|
176
245
|
removeManagedBlock(file, ['<!-- ', ' -->'], !tracked(file));
|
|
177
246
|
removed = true;
|
|
178
247
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
248
|
+
// a DESELECTED harness's shim is un-landed the same way its owner would clean it: entry-by-entry out of a
|
|
249
|
+
// config file the host agent shares with the user, whole-file only when the file is wholly ours.
|
|
250
|
+
const shimSites = HARNESSES.flatMap((h) => [
|
|
251
|
+
...(h.shimScope === 'tree' ? [[h.shimFile(proj), h.shimOwnership]] : []),
|
|
252
|
+
...((path) => path ? [[path, h.shimOwnership]] : [])(h.worktreeHookAnchor(proj)),
|
|
253
|
+
]);
|
|
254
|
+
for (const [file, ownership] of new Map(shimSites)) {
|
|
182
255
|
if (targets.treeShims.has(file) || targets.anchors.has(file) || !existsSync(file))
|
|
183
256
|
continue;
|
|
257
|
+
if (ownership === 'shared-json') {
|
|
258
|
+
removeManagedJsonHooks(file);
|
|
259
|
+
removed = true;
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
184
262
|
if (readFileSync(file, 'utf8').includes('dispatch.sh')) {
|
|
185
263
|
rmSync(file, { force: true });
|
|
186
264
|
removed = true;
|
|
@@ -266,6 +344,32 @@ export function materialize(proj = process.cwd()) {
|
|
|
266
344
|
const targets = resolveHarnessTargets(cfg.harnesses);
|
|
267
345
|
retiredAxisNotice(cfg); // [[residence]] — the vote axis is retired
|
|
268
346
|
const { selected, plugins } = partitionHarnesses(targets);
|
|
347
|
+
// WHERE a shim lives and WHICH toolchain it names are two questions, and only the first is per-tree.
|
|
348
|
+
//
|
|
349
|
+
// Residence is forced by the adapter: Codex reads one project shim shared by every linked worktree, while
|
|
350
|
+
// Claude reads project settings from the session's cwd only, so every tree carries its own copy.
|
|
351
|
+
//
|
|
352
|
+
// The toolchain is the SAME answer for both: the main checkout owns the hook path. It used to be answered
|
|
353
|
+
// per scope — the project shim took the checkout, tree-scoped shims took whichever install happened to be
|
|
354
|
+
// running materialize — and the consequence was invisible and constant. A session's worktree is created by
|
|
355
|
+
// the BACKEND's install, so its shim named the checkout; the first commit inside that worktree runs
|
|
356
|
+
// pre-commit → materialize with the WORKTREE's install, which rewrote the same shim to name the branch.
|
|
357
|
+
// Every session therefore ran its opening turns on one toolchain and silently switched to another mid-flight,
|
|
358
|
+
// with no receipt anywhere. Neither half was wrong on its own; having two answers was.
|
|
359
|
+
//
|
|
360
|
+
// The main checkout is the right one, and for the reason the project shim already gave: a worktree's CLI may
|
|
361
|
+
// write its own tree-local artifacts but may never replace the shared hook owner. A session worktree is a
|
|
362
|
+
// DESK, not a toolchain install — it has no node_modules of its own, so a tree-pointing shim makes the very
|
|
363
|
+
// first hook of a fresh session try to build the branch. Governance is the product's, not the branch's.
|
|
364
|
+
const checkout = mainCheckout(proj);
|
|
365
|
+
retireLegacyCodexAnchors(checkout);
|
|
366
|
+
const projectDispatch = existsSync(join(checkout, 'spec-cli', 'hooks', 'dispatch.sh'))
|
|
367
|
+
? join(checkout, 'spec-cli', 'hooks', 'dispatch.sh')
|
|
368
|
+
: DISPATCH;
|
|
369
|
+
const projectSpex = existsSync(join(checkout, 'spec-cli', 'bin', 'spex.mjs'))
|
|
370
|
+
? join(checkout, 'spec-cli', 'bin', 'spex.mjs')
|
|
371
|
+
: SPEX;
|
|
372
|
+
const shimFor = (h) => h.shim(projectDispatch, projectSpex);
|
|
269
373
|
const skillNodes = loadSkillConfig();
|
|
270
374
|
const agentNodes = loadAgentConfig();
|
|
271
375
|
const commandNodes = loadConfig();
|
|
@@ -297,19 +401,33 @@ export function materialize(proj = process.cwd()) {
|
|
|
297
401
|
throw new Error(`conflicting materialize targets for ${path}`);
|
|
298
402
|
targets.set(path, content);
|
|
299
403
|
};
|
|
404
|
+
const addShimTarget = (targets, path, shim) => {
|
|
405
|
+
const prior = targets.get(path);
|
|
406
|
+
if (prior !== undefined && prior.content !== shim.content)
|
|
407
|
+
throw new Error(`conflicting materialize targets for ${path}`);
|
|
408
|
+
targets.set(path, shim);
|
|
409
|
+
};
|
|
300
410
|
for (const h of selected) {
|
|
301
411
|
if (contract)
|
|
302
412
|
for (const f of h.contractFiles(proj))
|
|
303
413
|
addTarget(contractTargets, f, contract);
|
|
304
|
-
const shim = h
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
414
|
+
const shim = shimFor(h);
|
|
415
|
+
// Codex discovers the root-checkout shim through this worktree anchor, but also parses the anchor as a
|
|
416
|
+
// project config layer. A second copy of our dispatcher therefore runs the same PreToolUse event twice.
|
|
417
|
+
// Keep the anchor present for layer discovery while leaving its hook set empty; the root checkout remains
|
|
418
|
+
// the sole executable hook owner.
|
|
419
|
+
// Claude reads project settings from the session's cwd only (measured on Claude Code 2.1.241: a hook
|
|
420
|
+
// configured solely in the main checkout never fires inside a nested linked worktree), so every tree
|
|
421
|
+
// carries its own tree-scoped shim; a session launched at the root fires the root's, never both. Its
|
|
422
|
+
// RESIDENCE is per-tree; its command paths are the main checkout's, exactly like the project shim's.
|
|
423
|
+
const target = { ownership: h.shimOwnership, content: shim.content, hooks: shim.hooks };
|
|
424
|
+
if (h.shimScope === 'tree')
|
|
425
|
+
addShimTarget(treeShimTargets, h.shimFile(proj), target);
|
|
308
426
|
// a linked-worktree ANCHOR copy of the shim, when the harness needs one (codex: the shim lives at the main
|
|
309
427
|
// checkout, so the worktree gets no `.codex/` unless we place one). One adapter line; null otherwise.
|
|
310
428
|
const anchor = h.worktreeHookAnchor(proj);
|
|
311
429
|
if (anchor)
|
|
312
|
-
|
|
430
|
+
addShimTarget(anchorTargets, anchor, { ownership: 'exclusive', content: '{\n "hooks": {}\n}\n' });
|
|
313
431
|
}
|
|
314
432
|
for (const sk of skillNodes)
|
|
315
433
|
for (const h of selected) {
|
|
@@ -351,42 +469,57 @@ export function materialize(proj = process.cwd()) {
|
|
|
351
469
|
record('contract', file);
|
|
352
470
|
}
|
|
353
471
|
const contractPaths = [...contractTargets.keys()];
|
|
354
|
-
|
|
472
|
+
// A shim file's residence is the SAME live content fact a contract file's is ([[residence]]): wholly ours →
|
|
473
|
+
// a machine fact hidden by the tree's ignore block, exactly as before; carrying the user's own content (or
|
|
474
|
+
// already tracked) → left VISIBLE, because hiding a file they own is data-loss shaped. A visible shim means
|
|
475
|
+
// our hook commands — absolute paths to THIS machine's toolchain — sit in a file they may commit, so say so.
|
|
476
|
+
const visibleShims = [];
|
|
477
|
+
for (const [file, shim] of [...treeShimTargets, ...anchorTargets]) {
|
|
355
478
|
mkdirSync(dirname(file), { recursive: true });
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
machinePaths.push(file);
|
|
359
|
-
}
|
|
360
|
-
for (const [file, content] of anchorTargets) {
|
|
361
|
-
mkdirSync(dirname(file), { recursive: true });
|
|
362
|
-
writeFileIfChanged(file, content);
|
|
479
|
+
if (!landShim(file, shim))
|
|
480
|
+
continue;
|
|
363
481
|
record('shim', file);
|
|
364
|
-
|
|
482
|
+
const theirs = shim.ownership === 'shared-json' && (isTrackedHere(file) || sharedShimHasHostContent(file));
|
|
483
|
+
if (theirs)
|
|
484
|
+
visibleShims.push(file);
|
|
485
|
+
else
|
|
486
|
+
machinePaths.push(file);
|
|
365
487
|
}
|
|
488
|
+
if (visibleShims.length)
|
|
489
|
+
console.warn(`spexcode: ${visibleShims.map((f) => relative(proj, f)).join(', ')} carries your own configuration, so it stays visible to git — and it now also holds SpexCode's hook entries, whose commands are absolute paths to THIS machine's toolchain. Committing them would break the file for everyone else. Keep them out of your commits (each clone re-materializes its own), or adopt with "harnesses": [] and wire the hooks yourself.`);
|
|
366
490
|
const selectedByDispatch = new Map(selected.map((h) => [h.dispatchId, h]));
|
|
367
491
|
for (const h of selectedByDispatch.values()) {
|
|
368
|
-
const shim = h
|
|
492
|
+
const shim = shimFor(h);
|
|
369
493
|
if (h.shimScope === 'project') {
|
|
370
494
|
const file = h.shimFile(proj);
|
|
371
495
|
mkdirSync(dirname(file), { recursive: true });
|
|
372
|
-
|
|
373
|
-
|
|
496
|
+
if (landShim(file, { ownership: h.shimOwnership, content: shim.content, hooks: shim.hooks }))
|
|
497
|
+
record('shim', file);
|
|
374
498
|
}
|
|
375
499
|
for (const file of h.writeTrust(proj, shim.cmd))
|
|
376
500
|
record('trust', file);
|
|
377
501
|
}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
502
|
+
// A spec node named `distill` says WHICH path to write, never that the path is ours to take. An existing
|
|
503
|
+
// file with no GENERATED_MARK is the user's own same-named skill/agent — skip it and report the collision,
|
|
504
|
+
// the same identity gate the erase half has always applied. Skipped paths are NOT recorded and NOT excluded:
|
|
505
|
+
// the file is theirs in every respect, including how git sees it.
|
|
506
|
+
const collisions = [];
|
|
507
|
+
const plantGenerated = (kind, file, content) => {
|
|
508
|
+
if (!isGeneratedArtifact(file)) {
|
|
509
|
+
collisions.push(file);
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
385
512
|
mkdirSync(dirname(file), { recursive: true });
|
|
386
513
|
writeFileIfChanged(file, content);
|
|
387
514
|
artifactPaths.push(file);
|
|
388
|
-
record(
|
|
389
|
-
}
|
|
515
|
+
record(kind, file);
|
|
516
|
+
};
|
|
517
|
+
for (const [file, content] of skillTargets)
|
|
518
|
+
plantGenerated('skill', file, content);
|
|
519
|
+
for (const [file, content] of agentTargets)
|
|
520
|
+
plantGenerated('agent', file, content);
|
|
521
|
+
if (collisions.length)
|
|
522
|
+
console.warn(`spexcode: left ${collisions.length} file(s) untouched — you already have a file at that name and it is not SpexCode-generated: ${collisions.map((f) => relative(proj, f)).join(', ')}. Rename the colliding .spec node (or your file) if you want the generated one delivered.`);
|
|
390
523
|
// (8) the PLUGIN target ([[plugin-harness]]): materialize the whole system into one self-contained Claude-plugin
|
|
391
524
|
// bundle per selected folder. A plugin is EXCLUSIVE (`selected` is empty then). Pruning a DESELECTED
|
|
392
525
|
// folder needs the PREVIOUS folder set, which the live config no longer names — the one landing point
|
|
@@ -440,11 +573,19 @@ export function materialize(proj = process.cwd()) {
|
|
|
440
573
|
const ignoreFile = join(proj, '.gitignore');
|
|
441
574
|
const ignoreTracked = isTrackedHere(ignoreFile);
|
|
442
575
|
const ignoreHost = existsSync(ignoreFile) ? stripSpexcodeBlock(readFileSync(ignoreFile, 'utf8'), ['# ', '']) : '';
|
|
443
|
-
|
|
576
|
+
// the self-entry only earns its keep alongside a real one: with NOTHING selected ("harnesses": []) there is
|
|
577
|
+
// no artifact to hide, and a .gitignore whose whole content is a rule ignoring itself is pure footprint.
|
|
578
|
+
// An empty body then un-writes the block, taking a wholly-ours .gitignore with it.
|
|
579
|
+
if (localEntries.length && !ignoreTracked && !ignoreHost.trim())
|
|
444
580
|
localEntries.push('.gitignore');
|
|
445
581
|
const ignoreBody = entries(localEntries);
|
|
446
|
-
if (
|
|
447
|
-
|
|
582
|
+
if (ignoreBody) {
|
|
583
|
+
if (writeManagedBlock(ignoreFile, ignoreBody, ['# ', '']))
|
|
584
|
+
changedMaterialized.add(ignoreFile);
|
|
585
|
+
}
|
|
586
|
+
else {
|
|
587
|
+
removeManagedBlock(ignoreFile, ['# ', ''], !ignoreTracked && !ignoreHost.trim());
|
|
588
|
+
}
|
|
448
589
|
const payloads = filterContracts.map((file) => ({ file: relative(proj, file), content: contract }));
|
|
449
590
|
if (ignoreTracked || ignoreHost.trim())
|
|
450
591
|
payloads.push({ file: '.gitignore', content: ignoreBody });
|