@dzhechkov/harness-core 0.8.35 → 0.8.37
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/.dz-manifest.json +224 -104
- package/README.md +335 -10
- package/dist/agentdb-index.d.ts +87 -7
- package/dist/agentdb-index.d.ts.map +1 -1
- package/dist/agentdb-index.js +416 -57
- package/dist/agentdb-index.js.map +1 -1
- package/dist/apply-leg.d.ts +57 -1
- package/dist/apply-leg.d.ts.map +1 -1
- package/dist/apply-leg.js +450 -52
- package/dist/apply-leg.js.map +1 -1
- package/dist/codex-hooks-assets.d.ts.map +1 -1
- package/dist/codex-hooks-assets.js +67 -5
- package/dist/codex-hooks-assets.js.map +1 -1
- package/dist/codex-hooks.d.ts +13 -1
- package/dist/codex-hooks.d.ts.map +1 -1
- package/dist/codex-hooks.js +13 -1
- package/dist/codex-hooks.js.map +1 -1
- package/dist/codex-rollouts.d.ts +118 -0
- package/dist/codex-rollouts.d.ts.map +1 -0
- package/dist/codex-rollouts.js +297 -0
- package/dist/codex-rollouts.js.map +1 -0
- package/dist/cost-ledger.d.ts +56 -4
- package/dist/cost-ledger.d.ts.map +1 -1
- package/dist/cost-ledger.js +176 -20
- package/dist/cost-ledger.js.map +1 -1
- package/dist/cross-family-control.d.ts +345 -0
- package/dist/cross-family-control.d.ts.map +1 -0
- package/dist/cross-family-control.js +802 -0
- package/dist/cross-family-control.js.map +1 -0
- package/dist/debt-ratchet.d.ts +53 -0
- package/dist/debt-ratchet.d.ts.map +1 -0
- package/dist/debt-ratchet.js +107 -0
- package/dist/debt-ratchet.js.map +1 -0
- package/dist/embedding-config.d.ts +42 -0
- package/dist/embedding-config.d.ts.map +1 -1
- package/dist/embedding-config.js +106 -10
- package/dist/embedding-config.js.map +1 -1
- package/dist/feature-adr-checkpoints.d.ts +6 -0
- package/dist/feature-adr-checkpoints.d.ts.map +1 -1
- package/dist/feature-adr-checkpoints.js +29 -0
- package/dist/feature-adr-checkpoints.js.map +1 -1
- package/dist/feature-adr-decision-recall.d.ts +2 -2
- package/dist/feature-adr-decision-recall.d.ts.map +1 -1
- package/dist/feature-adr-decision-recall.js +5 -3
- package/dist/feature-adr-decision-recall.js.map +1 -1
- package/dist/feature-adr-envelope.d.ts +96 -0
- package/dist/feature-adr-envelope.d.ts.map +1 -0
- package/dist/feature-adr-envelope.js +183 -0
- package/dist/feature-adr-envelope.js.map +1 -0
- package/dist/feature-adr-routing.d.ts +64 -0
- package/dist/feature-adr-routing.d.ts.map +1 -1
- package/dist/feature-adr-routing.js +122 -2
- package/dist/feature-adr-routing.js.map +1 -1
- package/dist/feature-adr-stage-canon.d.ts +79 -0
- package/dist/feature-adr-stage-canon.d.ts.map +1 -0
- package/dist/feature-adr-stage-canon.js +117 -0
- package/dist/feature-adr-stage-canon.js.map +1 -0
- package/dist/index.d.ts +23 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -7
- package/dist/index.js.map +1 -1
- package/dist/loop-blobs.generated.js +4 -4
- package/dist/loop-blobs.generated.js.map +1 -1
- package/dist/mutation-gate.d.ts +51 -0
- package/dist/mutation-gate.d.ts.map +1 -1
- package/dist/mutation-gate.js +295 -0
- package/dist/mutation-gate.js.map +1 -1
- package/dist/operations.d.ts +1 -0
- package/dist/operations.d.ts.map +1 -1
- package/dist/operations.js +18 -2
- package/dist/operations.js.map +1 -1
- package/dist/publish.d.ts +59 -7
- package/dist/publish.d.ts.map +1 -1
- package/dist/publish.js +205 -32
- package/dist/publish.js.map +1 -1
- package/dist/qe-bridge.d.ts.map +1 -1
- package/dist/qe-bridge.js +4 -2
- package/dist/qe-bridge.js.map +1 -1
- package/dist/qe-findings.d.ts +107 -0
- package/dist/qe-findings.d.ts.map +1 -0
- package/dist/qe-findings.js +417 -0
- package/dist/qe-findings.js.map +1 -0
- package/dist/recap.d.ts +1 -1
- package/dist/recap.d.ts.map +1 -1
- package/dist/recap.js +4 -2
- package/dist/recap.js.map +1 -1
- package/dist/release-line.d.ts +16 -0
- package/dist/release-line.d.ts.map +1 -1
- package/dist/release-line.js +31 -0
- package/dist/release-line.js.map +1 -1
- package/dist/round.d.ts +74 -1
- package/dist/round.d.ts.map +1 -1
- package/dist/round.js +112 -4
- package/dist/round.js.map +1 -1
- package/dist/run-records.d.ts +60 -0
- package/dist/run-records.d.ts.map +1 -1
- package/dist/run-records.js +244 -2
- package/dist/run-records.js.map +1 -1
- package/dist/score.d.ts +44 -1
- package/dist/score.d.ts.map +1 -1
- package/dist/score.js +78 -5
- package/dist/score.js.map +1 -1
- package/dist/vector-tier.d.ts +34 -3
- package/dist/vector-tier.d.ts.map +1 -1
- package/dist/vector-tier.js +105 -14
- package/dist/vector-tier.js.map +1 -1
- package/package.json +2 -2
- package/sbom.json +403 -103
- package/src/agentdb-index.ts +423 -60
- package/src/apply-leg.ts +469 -50
- package/src/codex-hooks-assets.ts +67 -5
- package/src/codex-hooks.ts +13 -1
- package/src/codex-rollouts.ts +374 -0
- package/src/cost-ledger.ts +232 -24
- package/src/cross-family-control.ts +960 -0
- package/src/debt-ratchet.ts +143 -0
- package/src/embedding-config.ts +131 -10
- package/src/feature-adr-checkpoints.ts +29 -0
- package/src/feature-adr-decision-recall.ts +6 -4
- package/src/feature-adr-envelope.ts +242 -0
- package/src/feature-adr-routing.ts +139 -2
- package/src/feature-adr-stage-canon.ts +141 -0
- package/src/index.ts +66 -7
- package/src/loop-blobs.generated.ts +4 -4
- package/src/mutation-gate.ts +316 -0
- package/src/operations.ts +18 -3
- package/src/publish.ts +247 -30
- package/src/qe-bridge.ts +4 -2
- package/src/qe-findings.ts +463 -0
- package/src/recap.ts +10 -3
- package/src/release-line.ts +32 -0
- package/src/round.ts +165 -6
- package/src/run-records.ts +282 -2
- package/src/score.ts +115 -6
- package/src/vector-tier.ts +127 -14
package/src/index.ts
CHANGED
|
@@ -203,6 +203,9 @@ export {
|
|
|
203
203
|
mirrorPatternsToVector,
|
|
204
204
|
backfillVectorMirror,
|
|
205
205
|
mergeHybridHits,
|
|
206
|
+
compareHybridHits,
|
|
207
|
+
evidenceRank,
|
|
208
|
+
orderHitsForReRank,
|
|
206
209
|
recallHybrid,
|
|
207
210
|
teachGuard,
|
|
208
211
|
vectorTierStatus,
|
|
@@ -224,6 +227,7 @@ export type {
|
|
|
224
227
|
HybridRecall,
|
|
225
228
|
HybridRecallMode,
|
|
226
229
|
HybridHit,
|
|
230
|
+
HybridOrderKey,
|
|
227
231
|
RankedPattern,
|
|
228
232
|
VectorServiceOptions,
|
|
229
233
|
VectorTierStatus,
|
|
@@ -311,10 +315,10 @@ export {
|
|
|
311
315
|
segmentRun,
|
|
312
316
|
} from './eta.js';
|
|
313
317
|
export type { CheckpointObservation, EtaEstimate, EtaInput, IncompleteCoverageSample, RunSegment, StageDurationSample, StageSample } from './eta.js';
|
|
314
|
-
export { indexPatternsToAgentdb, resolveAgentdbPath, searchAgentdbPatterns, listAgentdbDzIds, resolveAgentdbEmbedder, resetAgentdbEmbedderCache, getAgentdbEmbedderCacheStats, cosineSimilarity, importVectorsToAgentdb, reindexAgentdbRows, bumpAgentdbUses, clearAgentdbQuarantine, deleteAgentdbByDzIds, readAgentdbRowsByTaskType, DZ_OWNED_TASK_TYPES, ensureAgentdbSchema, readStoreGeneration, bumpStoreGeneration } from './agentdb-index.js';
|
|
318
|
+
export { indexPatternsToAgentdb, resolveAgentdbPath, searchAgentdbPatterns, listAgentdbDzIds, resolveAgentdbEmbedder, resolveStoreEmbedDtype, resetAgentdbEmbedderCache, getAgentdbEmbedderCacheStats, cosineSimilarity, importVectorsToAgentdb, reindexAgentdbRows, bumpAgentdbUses, clearAgentdbQuarantine, deleteAgentdbByDzIds, readAgentdbRowsByTaskType, DZ_OWNED_TASK_TYPES, ensureAgentdbSchema, readStoreGeneration, bumpStoreGeneration, resolveTransformersModule } from './agentdb-index.js';
|
|
315
319
|
export type { AgentdbSearchHit, AgentdbSearchResult, AgentdbImportRow } from './agentdb-index.js';
|
|
316
|
-
export { DEFAULT_EMBED_MODEL, LEGACY_EMBED_MODEL, DEFAULT_EMBED_DIM, KNOWN_EMBED_DIMS, resolveEmbedModel, readEmbedManifest, writeEmbedManifest, embedManifestPath, legacyEmbedManifest } from './embedding-config.js';
|
|
317
|
-
export type { EmbedModelConfig, EmbedModelSource, EmbedManifest } from './embedding-config.js';
|
|
320
|
+
export { DEFAULT_EMBED_MODEL, LEGACY_EMBED_MODEL, DEFAULT_EMBED_DIM, KNOWN_EMBED_DIMS, KNOWN_EMBED_DTYPES, resolveEmbedModel, readEmbedManifest, writeEmbedManifest, embedManifestPath, legacyEmbedManifest, currentEmbedManifest, guardEmbedSpace, snapshotEmbedManifest } from './embedding-config.js';
|
|
321
|
+
export type { EmbedModelConfig, EmbedModelSource, EmbedManifest, EmbedDtype } from './embedding-config.js';
|
|
318
322
|
export { putBookKnowledge, queryBookKnowledge, bookKbPath } from './book-kb.js';
|
|
319
323
|
export type { BookKU, BookKUHit } from './book-kb.js';
|
|
320
324
|
export { applyReadonlyPragmas, classifySqliteReadFailure, warnOnce } from './sqlite-read-helpers.js';
|
|
@@ -500,10 +504,30 @@ export {
|
|
|
500
504
|
decideRecordWrite,
|
|
501
505
|
decideReadBack,
|
|
502
506
|
recordVerdictLine,
|
|
507
|
+
parseModelSpec,
|
|
503
508
|
} from './run-records.js';
|
|
509
|
+
export {
|
|
510
|
+
ENVELOPE_SCHEMA,
|
|
511
|
+
TASK_KINDS,
|
|
512
|
+
PRIORITIES as ENVELOPE_PRIORITIES,
|
|
513
|
+
TIERS as ENVELOPE_TIERS,
|
|
514
|
+
buildExperimentEnvelope,
|
|
515
|
+
validateExperimentEnvelope,
|
|
516
|
+
} from './feature-adr-envelope.js';
|
|
517
|
+
export type {
|
|
518
|
+
TaskKind,
|
|
519
|
+
EnvelopePriority,
|
|
520
|
+
EnvelopeTier,
|
|
521
|
+
ExperimentEnvelope,
|
|
522
|
+
ExperimentEnvelopeArms,
|
|
523
|
+
ExperimentEnvelopeChosen,
|
|
524
|
+
ExperimentEnvelopePolicy,
|
|
525
|
+
ExperimentEnvelopeEvaluator,
|
|
526
|
+
BuildExperimentEnvelopeInput,
|
|
527
|
+
} from './feature-adr-envelope.js';
|
|
504
528
|
export { decidePublishSigning, decidePostSigningVerification, decideSignableSet, publishSigningLine, signableSetLine } from './publish-signing.js';
|
|
505
529
|
export type { PublishSigningVerdict, PublishSigningDecision, SignableSetDecision } from './publish-signing.js';
|
|
506
|
-
export type { RecordKind, RecordVerdict, RecordDecision } from './run-records.js';
|
|
530
|
+
export type { RecordKind, RecordVerdict, RecordDecision, LedgerEnrichInput, LedgerPriceEntry, ParsedModelSpec } from './run-records.js';
|
|
507
531
|
export type { AmendmentAmbiguity, AmendmentRow, AmendmentVerdict, AmendmentResolution, AmendmentOutcome, AmendmentDecision, PlanCoverageGap } from './amendment-trace.js';
|
|
508
532
|
// contract-checklist (ADR-001): pure extraction, canonical rendering, typed report parsing, and
|
|
509
533
|
// exact per-item verification. Filesystem discovery/containment stays in harness-cli.
|
|
@@ -730,7 +754,8 @@ export type {
|
|
|
730
754
|
ChainDefectAges,
|
|
731
755
|
ChainedJournal,
|
|
732
756
|
} from './event-chain.js';
|
|
733
|
-
export { decideProvenance, environmentCanMintProvenance, publishArgv, discoverPackages, publishPackages, bumpPatch, compareVersions, findUnpackagedSkills, findUnpublishedWorkspaceFloors, rewriteWorkspaceSpecs, orderByDependencies, syncReadmeVersion, isChangelogEntryLine, changelogRegion } from './publish.js';
|
|
757
|
+
export { decideProvenance, environmentCanMintProvenance, publishArgv, discoverPackages, publishPackages, bumpPatch, compareVersions, findUnpackagedSkills, findUnpublishedWorkspaceFloors, rewriteWorkspaceSpecs, orderByDependencies, syncReadmeVersion, isChangelogEntryLine, changelogRegion, planReadmeVersionSync } from './publish.js';
|
|
758
|
+
export type { ReadmeVersionSyncPlan, ReadmeSyncRewrite } from './publish.js';
|
|
734
759
|
export { RELEASE_LINE_RE, findReleaseLine, rewriteReleaseLine } from './release-line.js';
|
|
735
760
|
export * from './course-staleness.js';
|
|
736
761
|
export { fetchAllDownloads } from './downloads.js';
|
|
@@ -906,6 +931,14 @@ export {
|
|
|
906
931
|
// p16-non-js-portability: the gate-script search chain's operator note (ADR-002/AM-7) and the
|
|
907
932
|
// dzBin absolutization (ADR-003). Named for the same reason as the three above.
|
|
908
933
|
refusalNoteFor,
|
|
934
|
+
shellQuote,
|
|
935
|
+
planBackupCmd,
|
|
936
|
+
planRestoreCmd,
|
|
937
|
+
planArchiveBackupCmd,
|
|
938
|
+
planSnapshotCmd,
|
|
939
|
+
snapshotBlock,
|
|
940
|
+
snapshotNumber,
|
|
941
|
+
parsePlanSnapshot,
|
|
909
942
|
normalizeDzBin,
|
|
910
943
|
// qe-bridge-claude: the bridge's path/slug hygiene reuses these rather than minting a second
|
|
911
944
|
// definition of "safe" (ADR-001 D5-A).
|
|
@@ -973,6 +1006,7 @@ export type {
|
|
|
973
1006
|
ParsedBaselineCapture,
|
|
974
1007
|
ParsedLandingSignal,
|
|
975
1008
|
PlanGateVerdict,
|
|
1009
|
+
PlanSnapshot,
|
|
976
1010
|
PlanGateCmdOpts,
|
|
977
1011
|
CodexReviewCommandInput,
|
|
978
1012
|
CodexReviewCommandResult,
|
|
@@ -1049,6 +1083,25 @@ export type {
|
|
|
1049
1083
|
WorkflowRunRecord,
|
|
1050
1084
|
WorkflowStageEntry,
|
|
1051
1085
|
} from './cost-ledger.js';
|
|
1086
|
+
// Canonical stage taxonomy (feature measurement-integrity, ADR-001 D1).
|
|
1087
|
+
export { CANONICAL_STAGES, STAGE_LABEL_RULES, canonicalStage } from './feature-adr-stage-canon.js';
|
|
1088
|
+
export type {
|
|
1089
|
+
CanonicalStage,
|
|
1090
|
+
KnownStageResult,
|
|
1091
|
+
StageCanonResult,
|
|
1092
|
+
StageLabelRule,
|
|
1093
|
+
UnknownStageResult,
|
|
1094
|
+
} from './feature-adr-stage-canon.js';
|
|
1095
|
+
// Codex rollout-log reader (feature measurement-integrity, ADR-001 D3).
|
|
1096
|
+
export { matchCodexRollouts, parseCodexRollout } from './codex-rollouts.js';
|
|
1097
|
+
export type {
|
|
1098
|
+
CodexRollout,
|
|
1099
|
+
CodexRolloutMatch,
|
|
1100
|
+
CodexRolloutMatchWindow,
|
|
1101
|
+
CodexRolloutParseError,
|
|
1102
|
+
CodexRolloutTotals,
|
|
1103
|
+
CodexRolloutTurn,
|
|
1104
|
+
} from './codex-rollouts.js';
|
|
1052
1105
|
export type {
|
|
1053
1106
|
ClaudeUsageModel,
|
|
1054
1107
|
UsageCalibrationChange,
|
|
@@ -1149,6 +1202,7 @@ export {
|
|
|
1149
1202
|
// Run-process scorecard (feature dz-score, Reading C) — scores the DISCIPLINE of a feature-adr run
|
|
1150
1203
|
// from its artifacts and folds immutable receipts into a chained aggregate. Descriptive-only,
|
|
1151
1204
|
// permanently: neither the single-run score nor the aggregate gates.
|
|
1205
|
+
export * from './qe-findings.js';
|
|
1152
1206
|
export * from './score.js';
|
|
1153
1207
|
export * from './recap.js';
|
|
1154
1208
|
export * from './provenance.js';
|
|
@@ -1263,8 +1317,13 @@ export * from './run-registry.js';
|
|
|
1263
1317
|
|
|
1264
1318
|
export { JOURNAL_KINDS, formatLine, parseLine, selectWindow, appendWitnessed } from './journal.js';
|
|
1265
1319
|
export type { JournalKind, JournalEvent, JournalLine, JournalIo } from './journal.js';
|
|
1266
|
-
export { openRound, closeRound, listRounds } from './round.js';
|
|
1267
|
-
export type { RoundState, RoundExecState, RoundLedgerRow } from './round.js';
|
|
1320
|
+
export { openRound, closeRound, listRounds, validateClosedRoundLedgerRow } from './round.js';
|
|
1321
|
+
export type { RoundState, RoundExecState, RoundLedgerRow, RoundReviewSidecar } from './round.js';
|
|
1268
1322
|
export { parseCodexTokens, classifyRoundExecOutcome, buildRoundExecRow } from './round-exec.js';
|
|
1269
1323
|
export type { RoundExecOutcome, RoundExecLedgerRow } from './round-exec.js';
|
|
1270
1324
|
export * from './run-cleanup.js';
|
|
1325
|
+
|
|
1326
|
+
export * from './cross-family-control.js';
|
|
1327
|
+
|
|
1328
|
+
export { debtRatchetVerdict, parsePinnedCeiling, ceilingUnreadableMessage } from './debt-ratchet.js';
|
|
1329
|
+
export type { DebtRatchetArgs, DebtRatchetVerdict, PinnedCeiling } from './debt-ratchet.js';
|
|
@@ -61,11 +61,11 @@ export const BLOBS: Record<string, LoopBlob> = {
|
|
|
61
61
|
"training-pairs": {
|
|
62
62
|
name: "training-pairs",
|
|
63
63
|
version: "1.2.0",
|
|
64
|
-
contentHash: "
|
|
64
|
+
contentHash: "e80c53755e8c2b0e08d3dabcd49cdfffb2283a5b3f650c06b76196936979caf6",
|
|
65
65
|
sourcePath: "packages/@dzhechkov/harness-core/src/feature-adr-checkpoints.ts",
|
|
66
66
|
requires: ["checkpoints"],
|
|
67
67
|
exports: ["TRAINPAIR_SCHEMA_VERSION","TRAINPAIR_MAX_IO_CHARS","trainingPairFamily","trainingPairPath","TRAINPAIR_PRIVACY_NOTE","buildTrainingPair","serializeTrainingPair","trainingPairAppendCmd","decideCaptureMode","captureFailureRecord","trainingPairBackfillCmd","TP_BACKFILL_OK","TP_BACKFILL_SKIP"],
|
|
68
|
-
code: "function decideCaptureMode(opts) {\n if (!opts.enabled)\n return 'skip-disabled';\n if (!Number.isInteger(opts.recordCount) || opts.recordCount <= 0)\n return 'skip-empty';\n return opts.resumed ? 'backfill' : 'capture';\n}\nfunction captureFailureRecord(stage, mode, reason, detail) {\n const normalizedStage = typeof stage === 'string' && stage.trim() !== '' ? stage : 'unknown';\n const normalizedMode = mode === 'capture' || mode === 'backfill' || mode === 'skip-disabled' || mode === 'skip-empty'\n ? mode\n : null;\n const normalizedReason = reason === 'threw' || reason === 'unserializable' || reason === 'unverified' || reason === 'backfill-unverified' || reason === 'empty-output'\n ? reason\n : 'threw';\n let normalizedDetail = null;\n if (detail !== null && detail !== undefined) {\n try {\n const text = String(detail);\n if (text !== '')\n normalizedDetail = text.length > 500 ? text.slice(0, 500) + '…' : text;\n }\n catch {\n normalizedDetail = null;\n }\n }\n return { stage: normalizedStage, mode: normalizedMode, reason: normalizedReason, detail: normalizedDetail };\n}\nconst TRAINPAIR_SCHEMA_VERSION = 'fa-trainpair-3';\nconst TRAINPAIR_MAX_IO_CHARS = 48000;\nfunction trainingPairFamily(spec) {\n return /codex|gpt|openai/i.test(String(spec ?? '')) ? 'codex' : 'claude';\n}\nfunction trainingPairPath(slug, stage) {\n return '.dz/fa-training/' + slug + '/' + stage + '.jsonl';\n}\nconst TRAINPAIR_PRIVACY_NOTE = \"feature-adr TRAINING PAIRS (backlog 70e0f083): per-stage SFT records - STAGE INPUT (full prompt/context) -> STAGE OUTPUT (artifact/result) -> EVALUATION (QE grade + injected lessons) with model+family provenance; one JSONL file per stage per slug. PRIVACY: pairs may contain TARGET-REPO CODE and full prompts. This directory is NOT gitignored yet by explicit owner decision - review contents before sharing or publishing anything that embeds it. ts is the CAPTURE time. On a record with captureMode: 'backfill' that is the RECONSTRUCTION time, NOT the stage's observation time — the original stage's timing lives in that run's .fa-state checkpoint.\";\nconst TP_PROFILE_MARKER_START = '<!-- dz:profile:start -->';\nconst TP_PROFILE_MARKER_END = '<!-- dz:profile:end -->';\nconst TP_PROFILE_REDACTED = '[dz:profile REDACTED]';\nfunction redactProfileBlock(text) {\n if (typeof text !== 'string' || text === '')\n return typeof text === 'string' ? text : '';\n let out = '';\n let rest = text;\n for (;;) {\n const start = rest.indexOf(TP_PROFILE_MARKER_START);\n if (start === -1)\n return out + rest;\n out += rest.slice(0, start) + TP_PROFILE_REDACTED;\n const end = rest.indexOf(TP_PROFILE_MARKER_END, start + TP_PROFILE_MARKER_START.length);\n if (end === -1)\n return out;\n rest = rest.slice(end + TP_PROFILE_MARKER_END.length);\n }\n}\nfunction coerceText(v) {\n if (typeof v === 'string')\n return v;\n if (v === null || v === undefined)\n return '';\n try {\n const s = JSON.stringify(v);\n return typeof s === 'string' ? s : String(v);\n }\n catch {\n return String(v);\n }\n}\nfunction normalizeTrainingPairBudget(raw) {\n try {\n if (raw === undefined)\n return { primary: 'claude', claude: 'normal', codex: 'normal', preset: 'unset' };\n if (raw === null || typeof raw !== 'object')\n return null;\n const value = raw;\n const primary = value.primary;\n const claude = value.claude;\n const codex = value.codex;\n if (primary !== 'claude' && primary !== 'codex')\n return null;\n if (claude !== 'normal' && claude !== 'eco')\n return null;\n if (codex !== 'normal' && codex !== 'eco')\n return null;\n if (value.preset === 'unset')\n return { primary, claude, codex, preset: 'unset' };\n let preset = 'custom';\n if (claude === 'normal' && codex === 'normal')\n preset = 'normal';\n else if (claude === 'eco' && codex === 'eco')\n preset = 'eco';\n else if (claude === 'eco' && codex === 'normal')\n preset = 'hybrid';\n return { primary, claude, codex, preset };\n }\n catch {\n return null;\n }\n}\nfunction buildTrainingPair(opts) {\n let input = redactProfileBlock(coerceText(opts.input));\n let output = redactProfileBlock(coerceText(opts.output));\n let truncated = null;\n if (input.length + output.length > TRAINPAIR_MAX_IO_CHARS) {\n truncated = { inputChars: input.length, outputChars: output.length, inputHash: fnv1a64(input), outputHash: fnv1a64(output) };\n const half = Math.floor(TRAINPAIR_MAX_IO_CHARS / 2);\n let inKeep = input.length;\n let outKeep = output.length;\n if (outKeep <= half)\n inKeep = TRAINPAIR_MAX_IO_CHARS - outKeep;\n else if (inKeep <= half)\n outKeep = TRAINPAIR_MAX_IO_CHARS - inKeep;\n else {\n inKeep = half;\n outKeep = TRAINPAIR_MAX_IO_CHARS - half;\n }\n if (inKeep < input.length)\n input = input.slice(0, inKeep) + '\\n…[TRUNCATED ' + (truncated.inputChars - inKeep) + ' chars — full-text fnv1a64=' + truncated.inputHash + ']';\n if (outKeep < output.length)\n output = output.slice(0, outKeep) + '\\n…[TRUNCATED ' + (truncated.outputChars - outKeep) + ' chars — full-text fnv1a64=' + truncated.outputHash + ']';\n }\n const ev = opts.evaluation || {};\n const pv = opts.provenance || {};\n return {\n schema: TRAINPAIR_SCHEMA_VERSION,\n slug: opts.slug,\n stage: opts.stage,\n ts: opts.ts === undefined ? null : opts.ts,\n input,\n output,\n evaluation: {\n grade: typeof ev.grade === 'string' && ev.grade.trim() !== '' ? ev.grade : null,\n gradedBy: typeof ev.gradedBy === 'string' && ev.gradedBy !== '' ? ev.gradedBy : null,\n lessonsInjected: Array.isArray(ev.lessonsInjected) ? ev.lessonsInjected.filter((s) => typeof s === 'string' && s !== '') : [],\n },\n provenance: {\n model: typeof pv.model === 'string' && pv.model !== '' ? pv.model : 'unknown',\n family: pv.family === 'claude' || pv.family === 'codex' ? pv.family : trainingPairFamily(pv.model),\n role: typeof pv.role === 'string' && pv.role !== '' ? pv.role : 'unknown',\n tokens: typeof pv.tokens === 'number' && Number.isFinite(pv.tokens) ? pv.tokens : null,\n minutes: typeof pv.minutes === 'number' && Number.isFinite(pv.minutes) ? pv.minutes : null,\n },\n budgetMode: normalizeTrainingPairBudget(opts.budgetMode),\n truncated,\n captureMode: opts.captureMode === 'backfill' ? 'backfill' : 'capture',\n resumed: opts.resumed === true,\n };\n}\nfunction serializeTrainingPair(pair) {\n try {\n const line = JSON.stringify(pair);\n return typeof line === 'string' ? line : null;\n }\n catch {\n return null;\n }\n}\nfunction trainingPairAppendCmd(repoAbs, slug, stage, line) {\n const dirAbs = repoAbs + '/.dz/fa-training/' + slug;\n const readmeAbs = repoAbs + '/.dz/fa-training/README.md';\n const fileAbs = dirAbs + '/' + stage + '.jsonl';\n return ('mkdir -p ' + shellQuote(dirAbs) +\n ' && { [ -f ' + shellQuote(readmeAbs) + ' ] || printf \\'%s\\\\n\\' ' + shellQuote(TRAINPAIR_PRIVACY_NOTE) + ' > ' + shellQuote(readmeAbs) + '; }' +\n \" && printf '%s\\\\n' \" + shellQuote(line) + ' >> ' + shellQuote(fileAbs));\n}\nconst TP_BACKFILL_OK = 'TP-BACKFILL-OK';\nconst TP_BACKFILL_SKIP = 'TP-BACKFILL-SKIP';\nconst TP_BACKFILL_DUP = 'TP-BACKFILL-DUP';\nfunction trainingPairBackfillCmd(repoAbs, slug, stage, lines, markKey) {\n if (typeof repoAbs !== 'string' || repoAbs === '')\n return null;\n if (typeof slug !== 'string' || slug === '')\n return null;\n if (typeof stage !== 'string' || stage === '')\n return null;\n if (!Array.isArray(lines) || lines.length === 0 || !lines.every(line => typeof line === 'string' && line !== ''))\n return null;\n const dirAbs = repoAbs + '/.dz/fa-training/' + slug;\n const readmeAbs = repoAbs + '/.dz/fa-training/README.md';\n const fileAbs = dirAbs + '/' + stage + '.jsonl';\n const markDir = repoAbs + '/.dz/fa-training/.backfill-marks';\n const markStage = stage.replace(/\\.\\./g, '_').replace(/\\//g, '_');\n const resolvedMarkKey = markKey === undefined ? fnv1a64(stage + '\\0' + lines.join('\\n')) : markKey;\n const markPath = markDir + '/' + markStage + '-' + resolvedMarkKey;\n const appends = lines\n .map(line => \"printf '%s\\\\n' \" + shellQuote(line) + ' >> ' + shellQuote(fileAbs))\n .join(' && ');\n return ('mkdir -p ' + shellQuote(dirAbs) +\n ' && { [ -f ' + shellQuote(readmeAbs) + ' ] || printf \\'%s\\\\n\\' ' + shellQuote(TRAINPAIR_PRIVACY_NOTE) + ' > ' + shellQuote(readmeAbs) + '; }' +\n ' && mkdir -p ' + shellQuote(markDir) +\n ' && if mkdir ' + shellQuote(markPath) + ' 2>/dev/null; then ' +\n 'if [ -f ' + shellQuote(fileAbs) + ' ]; then echo ' + shellQuote(TP_BACKFILL_SKIP) +\n '; else { ' + appends + ' && echo ' + shellQuote(TP_BACKFILL_OK) + '; } || { rmdir ' + shellQuote(markPath) + ' 2>/dev/null; false; }; fi' +\n '; else echo ' + shellQuote(TP_BACKFILL_DUP) + '; fi');\n}",
|
|
68
|
+
code: "function decideCaptureMode(opts) {\n if (!opts.enabled)\n return 'skip-disabled';\n if (!Number.isInteger(opts.recordCount) || opts.recordCount <= 0)\n return 'skip-empty';\n return opts.resumed ? 'backfill' : 'capture';\n}\nfunction captureFailureRecord(stage, mode, reason, detail) {\n const normalizedStage = typeof stage === 'string' && stage.trim() !== '' ? stage : 'unknown';\n const normalizedMode = mode === 'capture' || mode === 'backfill' || mode === 'skip-disabled' || mode === 'skip-empty'\n ? mode\n : null;\n const normalizedReason = reason === 'threw' || reason === 'unserializable' || reason === 'unverified' || reason === 'backfill-unverified' || reason === 'empty-output'\n ? reason\n : 'threw';\n let normalizedDetail = null;\n if (detail !== null && detail !== undefined) {\n try {\n const text = String(detail);\n if (text !== '')\n normalizedDetail = text.length > 500 ? text.slice(0, 500) + '…' : text;\n }\n catch {\n normalizedDetail = null;\n }\n }\n return { stage: normalizedStage, mode: normalizedMode, reason: normalizedReason, detail: normalizedDetail };\n}\nconst TRAINPAIR_SCHEMA_VERSION = 'fa-trainpair-3';\nconst TRAINPAIR_MAX_IO_CHARS = 48000;\nfunction trainingPairFamily(spec) {\n return /codex|gpt|openai/i.test(String(spec ?? '')) ? 'codex' : 'claude';\n}\nfunction trainingPairPath(slug, stage) {\n return '.dz/fa-training/' + slug + '/' + stage + '.jsonl';\n}\nconst TRAINPAIR_PRIVACY_NOTE = \"feature-adr TRAINING PAIRS (backlog 70e0f083): per-stage SFT records - STAGE INPUT (full prompt/context) -> STAGE OUTPUT (artifact/result) -> EVALUATION (QE grade + injected lessons) with model+family provenance; one JSONL file per stage per slug. PRIVACY: pairs may contain TARGET-REPO CODE and full prompts. This directory is NOT gitignored yet by explicit owner decision - review contents before sharing or publishing anything that embeds it. ts is the CAPTURE time. On a record with captureMode: 'backfill' that is the RECONSTRUCTION time, NOT the stage's observation time — the original stage's timing lives in that run's .fa-state checkpoint.\";\nconst TP_PROFILE_MARKER_START = '<!-- dz:profile:start -->';\nconst TP_PROFILE_MARKER_END = '<!-- dz:profile:end -->';\nconst TP_PROFILE_REDACTED = '[dz:profile REDACTED]';\nfunction redactProfileBlock(text) {\n if (typeof text !== 'string' || text === '')\n return typeof text === 'string' ? text : '';\n let out = '';\n let rest = text;\n for (;;) {\n const start = rest.indexOf(TP_PROFILE_MARKER_START);\n if (start === -1)\n return out + rest;\n out += rest.slice(0, start) + TP_PROFILE_REDACTED;\n const end = rest.indexOf(TP_PROFILE_MARKER_END, start + TP_PROFILE_MARKER_START.length);\n if (end === -1)\n return out;\n rest = rest.slice(end + TP_PROFILE_MARKER_END.length);\n }\n}\nfunction coerceText(v) {\n if (typeof v === 'string')\n return v;\n if (v === null || v === undefined)\n return '';\n try {\n const s = JSON.stringify(v);\n return typeof s === 'string' ? s : String(v);\n }\n catch {\n return String(v);\n }\n}\nfunction normalizeTrainingPairBudget(raw) {\n try {\n if (raw === undefined)\n return { primary: 'claude', claude: 'normal', codex: 'normal', preset: 'unset' };\n if (raw === null || typeof raw !== 'object')\n return null;\n const value = raw;\n const primary = value.primary;\n const claude = value.claude;\n const codex = value.codex;\n if (primary !== 'claude' && primary !== 'codex')\n return null;\n if (claude !== 'normal' && claude !== 'eco')\n return null;\n if (codex !== 'normal' && codex !== 'eco')\n return null;\n if (value.preset === 'unset')\n return { primary, claude, codex, preset: 'unset' };\n let preset = 'custom';\n if (claude === 'normal' && codex === 'normal')\n preset = 'normal';\n else if (claude === 'eco' && codex === 'eco')\n preset = 'eco';\n else if (claude === 'eco' && codex === 'normal')\n preset = 'hybrid';\n return { primary, claude, codex, preset };\n }\n catch {\n return null;\n }\n}\nfunction normalizeTrainingPairEnvelope(raw) {\n if (raw === null || raw === undefined || typeof raw !== 'object' || Array.isArray(raw))\n return null;\n const v = raw;\n if (v.schema !== 1)\n return null;\n if (typeof v.runId !== 'string' || v.runId.trim() === '')\n return null;\n if (v.arms === null || typeof v.arms !== 'object')\n return null;\n if (v.chosen === null || typeof v.chosen !== 'object')\n return null;\n if (v.policy === null || typeof v.policy !== 'object')\n return null;\n if (v.evaluator === null || typeof v.evaluator !== 'object')\n return null;\n return raw;\n}\nfunction buildTrainingPair(opts) {\n let input = redactProfileBlock(coerceText(opts.input));\n let output = redactProfileBlock(coerceText(opts.output));\n let truncated = null;\n if (input.length + output.length > TRAINPAIR_MAX_IO_CHARS) {\n truncated = { inputChars: input.length, outputChars: output.length, inputHash: fnv1a64(input), outputHash: fnv1a64(output) };\n const half = Math.floor(TRAINPAIR_MAX_IO_CHARS / 2);\n let inKeep = input.length;\n let outKeep = output.length;\n if (outKeep <= half)\n inKeep = TRAINPAIR_MAX_IO_CHARS - outKeep;\n else if (inKeep <= half)\n outKeep = TRAINPAIR_MAX_IO_CHARS - inKeep;\n else {\n inKeep = half;\n outKeep = TRAINPAIR_MAX_IO_CHARS - half;\n }\n if (inKeep < input.length)\n input = input.slice(0, inKeep) + '\\n…[TRUNCATED ' + (truncated.inputChars - inKeep) + ' chars — full-text fnv1a64=' + truncated.inputHash + ']';\n if (outKeep < output.length)\n output = output.slice(0, outKeep) + '\\n…[TRUNCATED ' + (truncated.outputChars - outKeep) + ' chars — full-text fnv1a64=' + truncated.outputHash + ']';\n }\n const ev = opts.evaluation || {};\n const pv = opts.provenance || {};\n return {\n schema: TRAINPAIR_SCHEMA_VERSION,\n slug: opts.slug,\n stage: opts.stage,\n ts: opts.ts === undefined ? null : opts.ts,\n input,\n output,\n evaluation: {\n grade: typeof ev.grade === 'string' && ev.grade.trim() !== '' ? ev.grade : null,\n gradedBy: typeof ev.gradedBy === 'string' && ev.gradedBy !== '' ? ev.gradedBy : null,\n lessonsInjected: Array.isArray(ev.lessonsInjected) ? ev.lessonsInjected.filter((s) => typeof s === 'string' && s !== '') : [],\n },\n provenance: {\n model: typeof pv.model === 'string' && pv.model !== '' ? pv.model : 'unknown',\n family: pv.family === 'claude' || pv.family === 'codex' ? pv.family : trainingPairFamily(pv.model),\n role: typeof pv.role === 'string' && pv.role !== '' ? pv.role : 'unknown',\n tokens: typeof pv.tokens === 'number' && Number.isFinite(pv.tokens) ? pv.tokens : null,\n minutes: typeof pv.minutes === 'number' && Number.isFinite(pv.minutes) ? pv.minutes : null,\n },\n budgetMode: normalizeTrainingPairBudget(opts.budgetMode),\n truncated,\n captureMode: opts.captureMode === 'backfill' ? 'backfill' : 'capture',\n resumed: opts.resumed === true,\n envelope: normalizeTrainingPairEnvelope(opts.envelope),\n };\n}\nfunction serializeTrainingPair(pair) {\n try {\n const line = JSON.stringify(pair);\n return typeof line === 'string' ? line : null;\n }\n catch {\n return null;\n }\n}\nfunction trainingPairAppendCmd(repoAbs, slug, stage, line) {\n const dirAbs = repoAbs + '/.dz/fa-training/' + slug;\n const readmeAbs = repoAbs + '/.dz/fa-training/README.md';\n const fileAbs = dirAbs + '/' + stage + '.jsonl';\n return ('mkdir -p ' + shellQuote(dirAbs) +\n ' && { [ -f ' + shellQuote(readmeAbs) + ' ] || printf \\'%s\\\\n\\' ' + shellQuote(TRAINPAIR_PRIVACY_NOTE) + ' > ' + shellQuote(readmeAbs) + '; }' +\n \" && printf '%s\\\\n' \" + shellQuote(line) + ' >> ' + shellQuote(fileAbs));\n}\nconst TP_BACKFILL_OK = 'TP-BACKFILL-OK';\nconst TP_BACKFILL_SKIP = 'TP-BACKFILL-SKIP';\nconst TP_BACKFILL_DUP = 'TP-BACKFILL-DUP';\nfunction trainingPairBackfillCmd(repoAbs, slug, stage, lines, markKey) {\n if (typeof repoAbs !== 'string' || repoAbs === '')\n return null;\n if (typeof slug !== 'string' || slug === '')\n return null;\n if (typeof stage !== 'string' || stage === '')\n return null;\n if (!Array.isArray(lines) || lines.length === 0 || !lines.every(line => typeof line === 'string' && line !== ''))\n return null;\n const dirAbs = repoAbs + '/.dz/fa-training/' + slug;\n const readmeAbs = repoAbs + '/.dz/fa-training/README.md';\n const fileAbs = dirAbs + '/' + stage + '.jsonl';\n const markDir = repoAbs + '/.dz/fa-training/.backfill-marks';\n const markStage = stage.replace(/\\.\\./g, '_').replace(/\\//g, '_');\n const resolvedMarkKey = markKey === undefined ? fnv1a64(stage + '\\0' + lines.join('\\n')) : markKey;\n const markPath = markDir + '/' + markStage + '-' + resolvedMarkKey;\n const appends = lines\n .map(line => \"printf '%s\\\\n' \" + shellQuote(line) + ' >> ' + shellQuote(fileAbs))\n .join(' && ');\n return ('mkdir -p ' + shellQuote(dirAbs) +\n ' && { [ -f ' + shellQuote(readmeAbs) + ' ] || printf \\'%s\\\\n\\' ' + shellQuote(TRAINPAIR_PRIVACY_NOTE) + ' > ' + shellQuote(readmeAbs) + '; }' +\n ' && mkdir -p ' + shellQuote(markDir) +\n ' && if mkdir ' + shellQuote(markPath) + ' 2>/dev/null; then ' +\n 'if [ -f ' + shellQuote(fileAbs) + ' ]; then echo ' + shellQuote(TP_BACKFILL_SKIP) +\n '; else { ' + appends + ' && echo ' + shellQuote(TP_BACKFILL_OK) + '; } || { rmdir ' + shellQuote(markPath) + ' 2>/dev/null; false; }; fi' +\n '; else echo ' + shellQuote(TP_BACKFILL_DUP) + '; fi');\n}",
|
|
69
69
|
},
|
|
70
70
|
"model-resolver": {
|
|
71
71
|
name: "model-resolver",
|
|
@@ -88,11 +88,11 @@ export const BLOBS: Record<string, LoopBlob> = {
|
|
|
88
88
|
"codex-dispatch": {
|
|
89
89
|
name: "codex-dispatch",
|
|
90
90
|
version: "1.0.0",
|
|
91
|
-
contentHash: "
|
|
91
|
+
contentHash: "dc50f313088cfcea9ec3c95051f73ba6311a8d4d9f962d638901e6f0187bdd32",
|
|
92
92
|
sourcePath: "packages/@dzhechkov/harness-core/src/feature-adr-routing.ts",
|
|
93
93
|
requires: [],
|
|
94
94
|
exports: ["codexDispatchMode","codexExecPlan","needsCodeLandedBarrier","decideCodeLanding"],
|
|
95
|
-
code: "const CODE_LANDING_PIPELINE_PREFIXES = ['features/', '.dz/', '.agentic-qe/', 'roam/'];\nfunction codeLandingEmptySignal(seconds) {\n return 'changed=0 after ' + seconds + 's — genuinely not landed';\n}\nfunction needsCodeLandedBarrier(coderUsed) {\n return coderUsed === 'codex' || coderUsed === 'codex-fallback';\n}\nfunction stripCodeLandingPath(path) {\n let p = String(path || '').trim().replace(/\\\\/g, '/');\n while (p.indexOf('./') === 0)\n p = p.slice(2);\n return p.replace(/\\/+/g, '/');\n}\nfunction classifyCodeLandingPathReject(path) {\n const p = stripCodeLandingPath(path);\n if (!p)\n return 'empty-after-strip';\n if (p[0] === '/')\n return 'absolute-path';\n if (p === '..' || p.indexOf('../') === 0 || p.indexOf('/../') >= 0 || p.endsWith('/..'))\n return 'traversal';\n if (/[\\0\\r\\n\\t \"'\\x60$;&|<>*?()[\\]{}!]/.test(p))\n return 'not-a-path';\n if (p.endsWith('/'))\n return 'not-a-path';\n for (const prefix of CODE_LANDING_PIPELINE_PREFIXES) {\n const bare = prefix.slice(0, -1);\n if (p === bare || p.indexOf(prefix) === 0)\n return 'pipeline-artifact-path';\n }\n return null;\n}\nfunction normalizeCodeLandingPath(path) {\n return classifyCodeLandingPathReject(path) === null ? stripCodeLandingPath(path) : '';\n}\nfunction filterPollableCodePaths(paths) {\n const out = [];\n const seen = new Set();\n for (const path of paths || []) {\n const normalized = normalizeCodeLandingPath(path);\n if (!normalized || seen.has(normalized))\n continue;\n seen.add(normalized);\n out.push(normalized);\n }\n return out;\n}\nfunction isNewlyChanged(path, baseline, currentHashes) {\n if (!baseline || !baseline.ok)\n return false;\n let recorded = null;\n for (const entry of baseline.entries) {\n if (entry.path === path) {\n recorded = entry.hash;\n break;\n }\n }\n if (recorded === null)\n return true;\n const now = currentHashes ? currentHashes[path] : undefined;\n if (now === undefined)\n return false;\n return now !== recorded;\n}\nfunction decideCodeLanding(snapshot) {\n const maxWaitMs = Math.max(0, snapshot.maxWaitMs);\n const elapsedMs = Math.max(0, snapshot.elapsedMs);\n const elapsedSeconds = Math.floor(elapsedMs / 1000);\n const expectedPaths = filterPollableCodePaths(snapshot.expectedPaths);\n const changedPaths = filterPollableCodePaths(snapshot.changedEntries.map(function (entry) { return entry.path; }));\n if (expectedPaths.length === 0) {\n return {\n status: 'inconclusive',\n reason: 'empty-plan-block',\n changed: 0,\n elapsedMs: elapsedMs,\n elapsedSeconds: elapsedSeconds,\n expectedPaths: expectedPaths,\n matchedExpectedPaths: [],\n changedPaths: changedPaths,\n predicate: 'no-expected-targets',\n qeSignalLine: 'CODEX-LANDING-SIGNAL status=inconclusive predicate=no-expected-targets reason=empty-plan-block',\n };\n }\n const baseline = snapshot.baseline;\n if (!baseline || !baseline.ok) {\n const reason = baseline && baseline.reason ? baseline.reason : 'no-baseline';\n return {\n status: 'inconclusive',\n reason: reason,\n changed: 0,\n elapsedMs: elapsedMs,\n elapsedSeconds: elapsedSeconds,\n expectedPaths: expectedPaths,\n matchedExpectedPaths: [],\n changedPaths: changedPaths,\n predicate: 'newly-changed',\n qeSignalLine: 'CODEX-LANDING-SIGNAL status=inconclusive predicate=newly-changed reason=' + reason,\n };\n }\n const changed = new Set(changedPaths);\n const matchedExpectedPaths = expectedPaths.filter(function (path) {\n return changed.has(path) && isNewlyChanged(path, baseline, snapshot.currentHashes);\n });\n if (matchedExpectedPaths.length > 0) {\n return {\n status: 'landed',\n changed: matchedExpectedPaths.length,\n elapsedMs: elapsedMs,\n elapsedSeconds: elapsedSeconds,\n expectedPaths: expectedPaths,\n matchedExpectedPaths: matchedExpectedPaths,\n changedPaths: changedPaths,\n predicate: 'newly-changed',\n qeSignalLine: 'CODEX-LANDING-SIGNAL status=landed changed=' +\n matchedExpectedPaths.length +\n ' after=' +\n elapsedSeconds +\n 's predicate=newly-changed matched=' +\n matchedExpectedPaths.join(','),\n };\n }\n if (elapsedMs < maxWaitMs) {\n return {\n status: 'not-yet-flushed',\n changed: 0,\n elapsedMs: elapsedMs,\n elapsedSeconds: elapsedSeconds,\n expectedPaths: expectedPaths,\n matchedExpectedPaths: [],\n changedPaths: changedPaths,\n predicate: 'empty-before-timeout',\n qeSignalLine: 'CODEX-LANDING-SIGNAL status=not-yet-flushed changed=0 after ' + elapsedSeconds + 's — not yet flushed',\n };\n }\n const terminalSeconds = Math.ceil(maxWaitMs / 1000);\n return {\n status: 'genuinely-not-landed',\n changed: 0,\n elapsedMs: elapsedMs,\n elapsedSeconds: terminalSeconds,\n expectedPaths: expectedPaths,\n matchedExpectedPaths: [],\n changedPaths: changedPaths,\n predicate: 'empty-after-timeout',\n qeSignalLine: 'CODEX-LANDING-SIGNAL status=genuinely-not-landed ' + codeLandingEmptySignal(terminalSeconds),\n };\n}\nconst WRAPPER_STAGES = { code: 1, plan: 1 };\nfunction codexDispatchMode(stage) {\n return WRAPPER_STAGES[stage] ? 'wrapper' : 'exec';\n}\nconst CODEX_EXEC_PROMPT_CEILING_CHARS = 24000;\nfunction codexExecPlan(input) {\n if (codexDispatchMode(input.stage) === 'wrapper') {\n return { mode: 'wrapper', reason: 'deliverable is a file written out-of-band' };\n }\n if (!input.probedId) {\n return { mode: 'claude', reason: 'no codex model id answered the probe' };\n }\n if (input.stage === 'qe' && input.scoped !== true) {\n return {\n mode: 'claude',\n reason: 'qe prompt is not SCOPED — an unscoped codex exec QE buys reconnaissance, not review ' +\n '(MEASURED 2026-08-21: 19038 chars, 280s, exit 124, no verdict)',\n };\n }\n if (input.promptChars > CODEX_EXEC_PROMPT_CEILING_CHARS) {\n return {\n mode: 'claude',\n reason: 'prompt is ' +\n input.promptChars +\n ' chars, over the ' +\n CODEX_EXEC_PROMPT_CEILING_CHARS +\n '-char codex exec ceiling (it would stall)',\n };\n }\n return { mode: 'exec', reason: 'codex exec on ' + input.probedId };\n}\nconst SCOPED_QE_MAX_FILES = 3;\nconst SCOPED_QE_MAX_QUESTIONS = 4;\nconst SCOPED_QE_MAX_PATH_CHARS = 200;\nconst SCOPED_QE_MAX_QUESTION_CHARS = 200;\nfunction scopedQePrompt(input) {\n const o = input || {};\n const rawFiles = Array.isArray(o.files) ? o.files : [];\n const files = [];\n for (const f of rawFiles) {\n const s = String(f === undefined || f === null ? '' : f).trim();\n if (s === '')\n continue;\n if (files.indexOf(s) !== -1)\n continue;\n files.push(s.slice(0, SCOPED_QE_MAX_PATH_CHARS));\n if (files.length >= SCOPED_QE_MAX_FILES)\n break;\n }\n if (files.length === 0)\n return '';\n const rawQuestions = Array.isArray(o.questions) ? o.questions : [];\n const questions = [];\n for (const q of rawQuestions) {\n const s = String(q === undefined || q === null ? '' : q).trim().replace(/\\s+/g, ' ');\n if (s === '')\n continue;\n questions.push(s.slice(0, SCOPED_QE_MAX_QUESTION_CHARS));\n if (questions.length >= SCOPED_QE_MAX_QUESTIONS)\n break;\n }\n if (questions.length === 0) {\n questions.push('Is this change correct, and does the test named by its ADR actually DISCRIMINATE (would it fail if the protection were deleted)?');\n }\n const slug = String(o.slug === undefined || o.slug === null ? '' : o.slug).trim().slice(0, 60);\n let out = 'Read ONLY these files: ' + files.join(', ') + '. Do NOT open any other file and do NOT explore the repository.';\n if (slug !== '')\n out += ' They are the changed files of feature ' + slug + '.';\n out += '\\n\\nAnswer these ' + questions.length + ' questions about them:\\n';\n for (let i = 0; i < questions.length; i++)\n out += i + 1 + '. ' + questions[i] + '\\n';\n out += '\\nFinish with
|
|
95
|
+
code: "const CODE_LANDING_PIPELINE_PREFIXES = ['features/', '.dz/', '.agentic-qe/', 'roam/'];\nfunction codeLandingEmptySignal(seconds) {\n return 'changed=0 after ' + seconds + 's — genuinely not landed';\n}\nfunction needsCodeLandedBarrier(coderUsed) {\n return coderUsed === 'codex' || coderUsed === 'codex-fallback';\n}\nfunction stripCodeLandingPath(path) {\n let p = String(path || '').trim().replace(/\\\\/g, '/');\n while (p.indexOf('./') === 0)\n p = p.slice(2);\n return p.replace(/\\/+/g, '/');\n}\nfunction classifyCodeLandingPathReject(path) {\n const p = stripCodeLandingPath(path);\n if (!p)\n return 'empty-after-strip';\n if (p[0] === '/')\n return 'absolute-path';\n if (p === '..' || p.indexOf('../') === 0 || p.indexOf('/../') >= 0 || p.endsWith('/..'))\n return 'traversal';\n if (/[\\0\\r\\n\\t \"'\\x60$;&|<>*?()[\\]{}!]/.test(p))\n return 'not-a-path';\n if (p.endsWith('/'))\n return 'not-a-path';\n for (const prefix of CODE_LANDING_PIPELINE_PREFIXES) {\n const bare = prefix.slice(0, -1);\n if (p === bare || p.indexOf(prefix) === 0)\n return 'pipeline-artifact-path';\n }\n return null;\n}\nfunction normalizeCodeLandingPath(path) {\n return classifyCodeLandingPathReject(path) === null ? stripCodeLandingPath(path) : '';\n}\nfunction filterPollableCodePaths(paths) {\n const out = [];\n const seen = new Set();\n for (const path of paths || []) {\n const normalized = normalizeCodeLandingPath(path);\n if (!normalized || seen.has(normalized))\n continue;\n seen.add(normalized);\n out.push(normalized);\n }\n return out;\n}\nfunction isNewlyChanged(path, baseline, currentHashes) {\n if (!baseline || !baseline.ok)\n return false;\n let recorded = null;\n for (const entry of baseline.entries) {\n if (entry.path === path) {\n recorded = entry.hash;\n break;\n }\n }\n if (recorded === null)\n return true;\n const now = currentHashes ? currentHashes[path] : undefined;\n if (now === undefined)\n return false;\n return now !== recorded;\n}\nfunction decideCodeLanding(snapshot) {\n const maxWaitMs = Math.max(0, snapshot.maxWaitMs);\n const elapsedMs = Math.max(0, snapshot.elapsedMs);\n const elapsedSeconds = Math.floor(elapsedMs / 1000);\n const expectedPaths = filterPollableCodePaths(snapshot.expectedPaths);\n const changedPaths = filterPollableCodePaths(snapshot.changedEntries.map(function (entry) { return entry.path; }));\n if (expectedPaths.length === 0) {\n return {\n status: 'inconclusive',\n reason: 'empty-plan-block',\n changed: 0,\n elapsedMs: elapsedMs,\n elapsedSeconds: elapsedSeconds,\n expectedPaths: expectedPaths,\n matchedExpectedPaths: [],\n changedPaths: changedPaths,\n predicate: 'no-expected-targets',\n qeSignalLine: 'CODEX-LANDING-SIGNAL status=inconclusive predicate=no-expected-targets reason=empty-plan-block',\n };\n }\n const baseline = snapshot.baseline;\n if (!baseline || !baseline.ok) {\n const reason = baseline && baseline.reason ? baseline.reason : 'no-baseline';\n return {\n status: 'inconclusive',\n reason: reason,\n changed: 0,\n elapsedMs: elapsedMs,\n elapsedSeconds: elapsedSeconds,\n expectedPaths: expectedPaths,\n matchedExpectedPaths: [],\n changedPaths: changedPaths,\n predicate: 'newly-changed',\n qeSignalLine: 'CODEX-LANDING-SIGNAL status=inconclusive predicate=newly-changed reason=' + reason,\n };\n }\n const changed = new Set(changedPaths);\n const matchedExpectedPaths = expectedPaths.filter(function (path) {\n return changed.has(path) && isNewlyChanged(path, baseline, snapshot.currentHashes);\n });\n if (matchedExpectedPaths.length > 0) {\n return {\n status: 'landed',\n changed: matchedExpectedPaths.length,\n elapsedMs: elapsedMs,\n elapsedSeconds: elapsedSeconds,\n expectedPaths: expectedPaths,\n matchedExpectedPaths: matchedExpectedPaths,\n changedPaths: changedPaths,\n predicate: 'newly-changed',\n qeSignalLine: 'CODEX-LANDING-SIGNAL status=landed changed=' +\n matchedExpectedPaths.length +\n ' after=' +\n elapsedSeconds +\n 's predicate=newly-changed matched=' +\n matchedExpectedPaths.join(','),\n };\n }\n if (elapsedMs < maxWaitMs) {\n return {\n status: 'not-yet-flushed',\n changed: 0,\n elapsedMs: elapsedMs,\n elapsedSeconds: elapsedSeconds,\n expectedPaths: expectedPaths,\n matchedExpectedPaths: [],\n changedPaths: changedPaths,\n predicate: 'empty-before-timeout',\n qeSignalLine: 'CODEX-LANDING-SIGNAL status=not-yet-flushed changed=0 after ' + elapsedSeconds + 's — not yet flushed',\n };\n }\n const terminalSeconds = Math.ceil(maxWaitMs / 1000);\n return {\n status: 'genuinely-not-landed',\n changed: 0,\n elapsedMs: elapsedMs,\n elapsedSeconds: terminalSeconds,\n expectedPaths: expectedPaths,\n matchedExpectedPaths: [],\n changedPaths: changedPaths,\n predicate: 'empty-after-timeout',\n qeSignalLine: 'CODEX-LANDING-SIGNAL status=genuinely-not-landed ' + codeLandingEmptySignal(terminalSeconds),\n };\n}\nconst WRAPPER_STAGES = { code: 1, plan: 1 };\nfunction codexDispatchMode(stage) {\n return WRAPPER_STAGES[stage] ? 'wrapper' : 'exec';\n}\nconst CODEX_EXEC_PROMPT_CEILING_CHARS = 24000;\nfunction codexExecPlan(input) {\n if (codexDispatchMode(input.stage) === 'wrapper') {\n return { mode: 'wrapper', reason: 'deliverable is a file written out-of-band' };\n }\n if (!input.probedId) {\n return { mode: 'claude', reason: 'no codex model id answered the probe' };\n }\n if (input.stage === 'qe' && input.scoped !== true) {\n return {\n mode: 'claude',\n reason: 'qe prompt is not SCOPED — an unscoped codex exec QE buys reconnaissance, not review ' +\n '(MEASURED 2026-08-21: 19038 chars, 280s, exit 124, no verdict)',\n };\n }\n if (input.promptChars > CODEX_EXEC_PROMPT_CEILING_CHARS) {\n return {\n mode: 'claude',\n reason: 'prompt is ' +\n input.promptChars +\n ' chars, over the ' +\n CODEX_EXEC_PROMPT_CEILING_CHARS +\n '-char codex exec ceiling (it would stall)',\n };\n }\n return { mode: 'exec', reason: 'codex exec on ' + input.probedId };\n}\nconst SCOPED_QE_MAX_FILES = 3;\nconst SCOPED_QE_MAX_QUESTIONS = 4;\nconst SCOPED_QE_MAX_PATH_CHARS = 200;\nconst SCOPED_QE_MAX_QUESTION_CHARS = 200;\nfunction scopedQePrompt(input) {\n const o = input || {};\n const rawFiles = Array.isArray(o.files) ? o.files : [];\n const files = [];\n for (const f of rawFiles) {\n const s = String(f === undefined || f === null ? '' : f).trim();\n if (s === '')\n continue;\n if (files.indexOf(s) !== -1)\n continue;\n files.push(s.slice(0, SCOPED_QE_MAX_PATH_CHARS));\n if (files.length >= SCOPED_QE_MAX_FILES)\n break;\n }\n if (files.length === 0)\n return '';\n const rawQuestions = Array.isArray(o.questions) ? o.questions : [];\n const questions = [];\n for (const q of rawQuestions) {\n const s = String(q === undefined || q === null ? '' : q).trim().replace(/\\s+/g, ' ');\n if (s === '')\n continue;\n questions.push(s.slice(0, SCOPED_QE_MAX_QUESTION_CHARS));\n if (questions.length >= SCOPED_QE_MAX_QUESTIONS)\n break;\n }\n if (questions.length === 0) {\n questions.push('Is this change correct, and does the test named by its ADR actually DISCRIMINATE (would it fail if the protection were deleted)?');\n }\n const slug = String(o.slug === undefined || o.slug === null ? '' : o.slug).trim().slice(0, 60);\n let out = 'Read ONLY these files: ' + files.join(', ') + '. Do NOT open any other file and do NOT explore the repository.';\n if (slug !== '')\n out += ' They are the changed files of feature ' + slug + '.';\n out += '\\n\\nAnswer these ' + questions.length + ' questions about them:\\n';\n for (let i = 0; i < questions.length; i++)\n out += i + 1 + '. ' + questions[i] + '\\n';\n out += '\\nFinish with two final lines: Grade: <A|B|C|D> and QE-VERDICT: <same>';\n return out;\n}",
|
|
96
96
|
},
|
|
97
97
|
"challenge-panel": {
|
|
98
98
|
name: "challenge-panel",
|
package/src/mutation-gate.ts
CHANGED
|
@@ -72,6 +72,322 @@ export function buildMutationTestCommand(
|
|
|
72
72
|
};
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
/**
|
|
76
|
+
* mutation-gate-inject-tokens FR-1..FR-3 (D1-D4b, fix-round 1 F1-F3): inject `--maxWorkers=<n>`
|
|
77
|
+
* into EVERY `vitest run` segment of a (possibly compound) test command, token-scoped — not the
|
|
78
|
+
* whole-command substring append that used to (a) cap only the FIRST `vitest run` in `a && b` (D1),
|
|
79
|
+
* (b) let an existing-flag SUBSTRING check be fooled by an unrelated `--maxWorkers` living inside a
|
|
80
|
+
* quoted argument or another command's own flags (D2, D3), and (c) stack a second `--maxWorkers` on
|
|
81
|
+
* top of a user's own `--max-workers=<n>` (D4) instead of deferring to it.
|
|
82
|
+
*
|
|
83
|
+
* Segmentation splits on `&&`/`||`/`;`/`|` that are OUTSIDE single/double quotes, POSIX-escape aware
|
|
84
|
+
* (fix-round 1, F1 — the round-1 Codex review's HIGH finding): outside single quotes a `\` makes the
|
|
85
|
+
* NEXT character literal (so `\"` cannot open/close a double-quoted span, and `\&` cannot be mistaken
|
|
86
|
+
* for an operator); inside double quotes a `\` escapes at least `\"` and `\\` (so `-t "a \" && b"` stays
|
|
87
|
+
* ONE segment — the escaped quote does not close the string, so the `&&` inside it is never treated as
|
|
88
|
+
* a real terminator); inside single quotes nothing is special, matching POSIX. This is the fix for the
|
|
89
|
+
* exploit the verdict named: `npx vitest run -t "a \" && b" --maxWorkers=1` used to be mis-split into
|
|
90
|
+
* two pseudo-segments (the existing flag ending up in the "wrong" one), stacking a second flag.
|
|
91
|
+
*
|
|
92
|
+
* Within each segment, `vitest run` is recognised only in COMMAND POSITION (fix-round 1, F2 — the
|
|
93
|
+
* round-1 Codex review's other HIGH finding): the first word token, after skipping any leading bare
|
|
94
|
+
* `NAME=value` assignments and at most one runner-prefix chain (`npx`, `pnpm exec`, `pnpm dlx`, `yarn`,
|
|
95
|
+
* `bunx`, or `env`/`cross-env` followed by more assignments), must have a BASENAME of `vitest`,
|
|
96
|
+
* `vitest.cmd`, `vitest.mjs` or `vitest.js` (a full path like `node_modules/.bin/vitest` counts — only
|
|
97
|
+
* the basename is compared), immediately followed by the literal token `run`. `echo vitest run` and
|
|
98
|
+
* `node wrapper.js vitest run` are therefore NOT vitest commands (`echo`/`node` is not an allowed
|
|
99
|
+
* prefix and is not itself a vitest basename) — the old scan matched `vitest`+`run` ANYWHERE in the
|
|
100
|
+
* segment and would have mutated both.
|
|
101
|
+
*
|
|
102
|
+
* An existing ceiling flag is detected per DECODED token (fix-round 1, F3 — MEDIUM finding: the old
|
|
103
|
+
* substring/regex checks compared RAW tokens, so a quoted `"--maxWorkers=1"` was invisible, and the
|
|
104
|
+
* regex additionally accepted unclaimed spellings like `--maxworkers`/`--max-Workers`) via
|
|
105
|
+
* `/^--(?:maxWorkers|max-workers)(?:=.*)?$/` — matches exactly `--maxWorkers=1`, `--maxWorkers 1` (the
|
|
106
|
+
* bare flag token, value in the next token), `--max-workers=1`, `--max-workers 1`; does NOT match
|
|
107
|
+
* `--maxWorkersFoo=9` (D3) or `--maxworkers`/`--max-Workers` (not the two claimed spellings). The scan
|
|
108
|
+
* stops at a standalone `--` token (F3): everything after it is positional per POSIX, so a `--maxWorkers`
|
|
109
|
+
* living there is a positional argument to vitest's OWN test-name filter, not a flag naming the ceiling.
|
|
110
|
+
* When a real flag is present, the segment is left untouched (the user's explicit choice wins, D4);
|
|
111
|
+
* when absent, ` --maxWorkers=<n>` is inserted immediately after the `run` token's RAW source span
|
|
112
|
+
* (never the decoded one — insertion always preserves the original quoting of everything else).
|
|
113
|
+
*/
|
|
114
|
+
export interface InjectVitestWorkerCeilingResult {
|
|
115
|
+
/** the command with the ceiling injected into every eligible vitest segment. */
|
|
116
|
+
readonly cmd: string;
|
|
117
|
+
/** how many segments were recognised as `vitest run` (0 for a non-vitest command). */
|
|
118
|
+
readonly vitestSegments: number;
|
|
119
|
+
/** how many of those segments actually got `--maxWorkers=<n>` inserted (excludes ones that already named it). */
|
|
120
|
+
readonly injected: number;
|
|
121
|
+
/** segments where `vitest run` was found only by the LOOSE token-pair fallback (command position
|
|
122
|
+
* unrecognised) — the CLI reports these so an odd wrapper shape is visible, not silent. */
|
|
123
|
+
readonly looseSegments: number;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
interface RawSegment {
|
|
127
|
+
readonly text: string;
|
|
128
|
+
/** the operator that ended this segment (`&&`/`||`/`;`/`|`), or `''` for the last segment. */
|
|
129
|
+
readonly terminator: string;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Split on unquoted `&&`/`||`/`;`/`|`, preserving each segment's own text (incl. surrounding
|
|
134
|
+
* whitespace). POSIX-escape aware (fix-round 1, F1): outside quotes `\` makes the next character
|
|
135
|
+
* literal (so it can neither open a quote nor start an operator); inside double quotes `\"` and `\\`
|
|
136
|
+
* are recognised escapes that do NOT close the string; inside single quotes nothing is escaped.
|
|
137
|
+
*/
|
|
138
|
+
function splitUnquotedSegments(cmd: string): RawSegment[] {
|
|
139
|
+
const segments: RawSegment[] = [];
|
|
140
|
+
let segStart = 0;
|
|
141
|
+
let inSingle = false;
|
|
142
|
+
let inDouble = false;
|
|
143
|
+
let i = 0;
|
|
144
|
+
while (i < cmd.length) {
|
|
145
|
+
const ch = cmd[i];
|
|
146
|
+
if (inSingle) {
|
|
147
|
+
if (ch === "'") inSingle = false;
|
|
148
|
+
i += 1;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (inDouble) {
|
|
152
|
+
if (ch === '"') { inDouble = false; i += 1; continue; }
|
|
153
|
+
if (ch === '\\') {
|
|
154
|
+
const next = cmd[i + 1];
|
|
155
|
+
// at least \" and \\ (F1's floor) — an escaped quote must not close the double-quoted span.
|
|
156
|
+
if (next === '"' || next === '\\') { i += 2; continue; }
|
|
157
|
+
i += 1;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
i += 1;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (ch === "'") {
|
|
164
|
+
inSingle = true;
|
|
165
|
+
i += 1;
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (ch === '"') {
|
|
169
|
+
inDouble = true;
|
|
170
|
+
i += 1;
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (ch === '\\') {
|
|
174
|
+
// Outside any quote, POSIX makes the character AFTER `\` literal — skip both so it can never
|
|
175
|
+
// be mis-read as a quote-open or an operator boundary.
|
|
176
|
+
i += cmd[i + 1] !== undefined ? 2 : 1;
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
const terminatorMatch = /^(&&|\|\||;|\|)/.exec(cmd.slice(i));
|
|
180
|
+
if (terminatorMatch) {
|
|
181
|
+
segments.push({ text: cmd.slice(segStart, i), terminator: terminatorMatch[0] });
|
|
182
|
+
i += terminatorMatch[0].length;
|
|
183
|
+
segStart = i;
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
i += 1;
|
|
187
|
+
}
|
|
188
|
+
segments.push({ text: cmd.slice(segStart), terminator: '' });
|
|
189
|
+
return segments;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
interface SegmentToken {
|
|
193
|
+
/** decoded value — quotes stripped, at-least-\"/\\ escapes resolved (F1, F3: comparisons use this). */
|
|
194
|
+
readonly value: string;
|
|
195
|
+
/** RAW source end offset within the segment text — insertion always splices at a raw offset. */
|
|
196
|
+
readonly end: number;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function isWhitespaceChar(ch: string | undefined): boolean {
|
|
200
|
+
return ch !== undefined && /\s/u.test(ch);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Whitespace-delimited tokens with POSIX-ish quote/escape DECODING (fix-round 1, F1/F3): a quoted
|
|
205
|
+
* span (single or double) still forms one token with its surrounding unquoted parts (POSIX word
|
|
206
|
+
* concatenation — `'it''s'` decodes to the single token `its`), but `token.value` now holds the
|
|
207
|
+
* DECODED text (quotes removed, `\"`/`\\` resolved inside double quotes, `\<char>` resolved to
|
|
208
|
+
* `<char>` outside any quote, single-quoted content kept verbatim) while `token.end` keeps the RAW
|
|
209
|
+
* source offset so `injectIntoSegment` can still splice into the ORIGINAL text unchanged elsewhere.
|
|
210
|
+
*/
|
|
211
|
+
function tokenizeSegment(text: string): SegmentToken[] {
|
|
212
|
+
const tokens: SegmentToken[] = [];
|
|
213
|
+
let i = 0;
|
|
214
|
+
while (i < text.length) {
|
|
215
|
+
while (i < text.length && isWhitespaceChar(text[i])) i += 1;
|
|
216
|
+
if (i >= text.length) break;
|
|
217
|
+
const start = i;
|
|
218
|
+
let value = '';
|
|
219
|
+
let inSingle = false;
|
|
220
|
+
let inDouble = false;
|
|
221
|
+
while (i < text.length) {
|
|
222
|
+
const ch = text[i];
|
|
223
|
+
if (ch === undefined) break;
|
|
224
|
+
if (inSingle) {
|
|
225
|
+
if (ch === "'") { inSingle = false; i += 1; continue; }
|
|
226
|
+
value += ch;
|
|
227
|
+
i += 1;
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
if (inDouble) {
|
|
231
|
+
if (ch === '"') { inDouble = false; i += 1; continue; }
|
|
232
|
+
if (ch === '\\') {
|
|
233
|
+
const next = text[i + 1];
|
|
234
|
+
if (next === '"' || next === '\\') { value += next; i += 2; continue; }
|
|
235
|
+
// not one of the two claimed double-quote escapes: the backslash is literal (F1's floor).
|
|
236
|
+
value += ch;
|
|
237
|
+
i += 1;
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
value += ch;
|
|
241
|
+
i += 1;
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
if (ch === "'") { inSingle = true; i += 1; continue; }
|
|
245
|
+
if (ch === '"') { inDouble = true; i += 1; continue; }
|
|
246
|
+
if (ch === '\\') {
|
|
247
|
+
const next = text[i + 1];
|
|
248
|
+
// POSIX line continuation (lead delta after Codex r2, MEDIUM): `\<newline>` (and `\<CR><LF>`)
|
|
249
|
+
// is REMOVED by the shell, never a literal — a token must not swallow a newline as its value.
|
|
250
|
+
if (next === '\n' || (next === '\r' && text[i + 2] === '\n')) {
|
|
251
|
+
i += next === '\n' ? 2 : 3;
|
|
252
|
+
// an EMPTY token so far means the continuation sat between words: skip the whitespace
|
|
253
|
+
// that follows so the next word starts a real token instead of an empty one.
|
|
254
|
+
if (value.length === 0) { while (i < text.length && isWhitespaceChar(text[i]!)) i += 1; }
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
if (next !== undefined) { value += next; i += 2; continue; }
|
|
258
|
+
value += ch; // trailing lone backslash: nothing to escape, keep it literal.
|
|
259
|
+
i += 1;
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
if (isWhitespaceChar(ch)) break;
|
|
263
|
+
value += ch;
|
|
264
|
+
i += 1;
|
|
265
|
+
}
|
|
266
|
+
tokens.push({ value, end: i });
|
|
267
|
+
}
|
|
268
|
+
return tokens;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** strictly `--maxWorkers` or `--max-workers`, with or without `=…` (fix-round 1, F3: no other spelling). */
|
|
272
|
+
const MAX_WORKERS_TOKEN = /^--(?:maxWorkers|max-workers)(?:=.*)?$/u;
|
|
273
|
+
|
|
274
|
+
/** basenames vitest's own bin may resolve to; a leading path (POSIX or Windows-style) is stripped. */
|
|
275
|
+
const VITEST_BASENAMES = new Set(['vitest', 'vitest.cmd', 'vitest.mjs', 'vitest.js']);
|
|
276
|
+
|
|
277
|
+
/** allowed single-token runner prefixes that may precede the vitest executable. */
|
|
278
|
+
const SINGLE_TOKEN_PREFIXES = new Set(['npx', 'yarn', 'bunx']);
|
|
279
|
+
|
|
280
|
+
/** `env`/`cross-env` may be followed by more `NAME=value` assignments before the real command. */
|
|
281
|
+
const ENV_STYLE_PREFIXES = new Set(['env', 'cross-env']);
|
|
282
|
+
|
|
283
|
+
/** `NAME=value` — a bare shell-style assignment token, decoded value. */
|
|
284
|
+
const ASSIGNMENT_TOKEN = /^[A-Za-z_][A-Za-z0-9_]*=/u;
|
|
285
|
+
|
|
286
|
+
function basenameOf(path: string): string {
|
|
287
|
+
const normalised = path.replace(/\\/g, '/');
|
|
288
|
+
const idx = normalised.lastIndexOf('/');
|
|
289
|
+
return idx === -1 ? normalised : normalised.slice(idx + 1);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Find `vitest run` in COMMAND POSITION (fix-round 1, F2): the first word token after (a) any
|
|
294
|
+
* leading bare `NAME=value` assignments, then (b) AT MOST ONE recognised runner-prefix chain
|
|
295
|
+
* (`npx` / `yarn` / `bunx` — one token; `pnpm exec` / `pnpm dlx` — two tokens; `env` / `cross-env` —
|
|
296
|
+
* one token, itself followed by zero or more further assignments) must have a basename in
|
|
297
|
+
* `VITEST_BASENAMES`, and the token right after it must be the literal `run`. Returns the INDEX of
|
|
298
|
+
* that `run` token, or null when this segment is not a vitest-run command. `echo vitest run` and
|
|
299
|
+
* `node wrapper.js vitest run` correctly return null: `echo`/`node` are neither an allowed prefix
|
|
300
|
+
* nor a vitest basename, so the scan never advances past them.
|
|
301
|
+
*/
|
|
302
|
+
function findVitestRunCommandIndex(tokens: readonly SegmentToken[]): number | null {
|
|
303
|
+
let idx = 0;
|
|
304
|
+
// Lead delta after Codex r2 (HIGH): the prefix chain is ITERATIVE and OPTION-TOLERANT — a runner
|
|
305
|
+
// prefix may carry its own dash-options (`npx --yes`, `pnpm exec --silent`) and prefixes may chain
|
|
306
|
+
// (`env CI=1 npx vitest run`). The previous single-step grammar returned null for both and left
|
|
307
|
+
// such commands UNCAPPED — a regression against the substring era this feature replaced.
|
|
308
|
+
for (;;) {
|
|
309
|
+
while (idx < tokens.length && ASSIGNMENT_TOKEN.test(tokens[idx]!.value)) idx += 1;
|
|
310
|
+
const head = tokens[idx]?.value;
|
|
311
|
+
if (head === undefined) break;
|
|
312
|
+
if (SINGLE_TOKEN_PREFIXES.has(head)) {
|
|
313
|
+
idx += 1;
|
|
314
|
+
} else if (head === 'pnpm' && (tokens[idx + 1]?.value === 'exec' || tokens[idx + 1]?.value === 'dlx')) {
|
|
315
|
+
idx += 2;
|
|
316
|
+
} else if (ENV_STYLE_PREFIXES.has(head)) {
|
|
317
|
+
idx += 1;
|
|
318
|
+
continue; // assignments after env/cross-env are consumed by the loop head
|
|
319
|
+
} else {
|
|
320
|
+
break;
|
|
321
|
+
}
|
|
322
|
+
while (idx < tokens.length && tokens[idx]!.value.startsWith('-')) idx += 1; // the prefix's own options
|
|
323
|
+
}
|
|
324
|
+
const exe = tokens[idx];
|
|
325
|
+
const runToken = tokens[idx + 1];
|
|
326
|
+
if (exe === undefined || runToken === undefined) return null;
|
|
327
|
+
if (!VITEST_BASENAMES.has(basenameOf(exe.value))) return null;
|
|
328
|
+
if (runToken.value !== 'run') return null;
|
|
329
|
+
return idx + 1;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* True when a `--maxWorkers`/`--max-workers` token names the ceiling ANYWHERE before a standalone
|
|
334
|
+
* `--` (fix-round 1, F3): a `--` marks POSIX end-of-options, so anything naming the flag AFTER it is
|
|
335
|
+
* a positional argument (e.g. vitest's own test-name filter), never the ceiling flag itself.
|
|
336
|
+
*/
|
|
337
|
+
function hasMaxWorkersFlag(tokens: readonly SegmentToken[]): boolean {
|
|
338
|
+
for (const token of tokens) {
|
|
339
|
+
if (token.value === '--') return false;
|
|
340
|
+
if (MAX_WORKERS_TOKEN.test(token.value)) return true;
|
|
341
|
+
}
|
|
342
|
+
return false;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* LOOSE fallback (lead delta after Codex r2, HIGH): when the strict command-position grammar finds
|
|
347
|
+
* nothing, look for a `<vitest-basename> run` token pair ANYWHERE in the segment. The ceiling exists
|
|
348
|
+
* to keep a full-suite run from taking the machine down (0bb74d66); an unrecognised wrapper shape
|
|
349
|
+
* must degrade to the substring-era behaviour (capped, reported as `loose`), never to an uncapped
|
|
350
|
+
* run. The price — `echo vitest run` also gets the flag — is named in the result so the CLI can say
|
|
351
|
+
* it out loud, and is a harmless extra argument to a non-vitest command.
|
|
352
|
+
*/
|
|
353
|
+
function findVitestRunLooseIndex(tokens: readonly SegmentToken[]): number | null {
|
|
354
|
+
for (let i = 0; i + 1 < tokens.length; i += 1) {
|
|
355
|
+
if (VITEST_BASENAMES.has(basenameOf(tokens[i]!.value)) && tokens[i + 1]!.value === 'run') return i + 1;
|
|
356
|
+
}
|
|
357
|
+
return null;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function injectIntoSegment(text: string, maxWorkers: number): { readonly text: string; readonly isVitest: boolean; readonly injected: boolean; readonly loose: boolean } {
|
|
361
|
+
const tokens = tokenizeSegment(text);
|
|
362
|
+
const strictIdx = findVitestRunCommandIndex(tokens);
|
|
363
|
+
const looseIdx = strictIdx === null ? findVitestRunLooseIndex(tokens) : null;
|
|
364
|
+
const runTokenIdx = strictIdx ?? looseIdx;
|
|
365
|
+
const loose = strictIdx === null && looseIdx !== null;
|
|
366
|
+
if (runTokenIdx === null) return { text, isVitest: false, injected: false, loose: false };
|
|
367
|
+
if (hasMaxWorkersFlag(tokens)) return { text, isVitest: true, injected: false, loose };
|
|
368
|
+
const runToken = tokens[runTokenIdx];
|
|
369
|
+
// unreachable defensively: runTokenIdx was derived from a valid index into `tokens` above.
|
|
370
|
+
if (runToken === undefined) return { text, isVitest: true, injected: false, loose };
|
|
371
|
+
const insertAt = runToken.end;
|
|
372
|
+
const injectedText = `${text.slice(0, insertAt)} --maxWorkers=${maxWorkers}${text.slice(insertAt)}`;
|
|
373
|
+
return { text: injectedText, isVitest: true, injected: true, loose };
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
export function injectVitestWorkerCeiling(testCmd: string, maxWorkers: number): InjectVitestWorkerCeilingResult {
|
|
377
|
+
const segments = splitUnquotedSegments(testCmd);
|
|
378
|
+
let vitestSegments = 0;
|
|
379
|
+
let injected = 0;
|
|
380
|
+
let looseSegments = 0;
|
|
381
|
+
const rebuilt = segments.map((segment) => {
|
|
382
|
+
const result = injectIntoSegment(segment.text, maxWorkers);
|
|
383
|
+
if (result.isVitest) vitestSegments += 1;
|
|
384
|
+
if (result.injected) injected += 1;
|
|
385
|
+
if (result.loose) looseSegments += 1;
|
|
386
|
+
return result.text + segment.terminator;
|
|
387
|
+
}).join('');
|
|
388
|
+
return { cmd: rebuilt, vitestSegments, injected, looseSegments };
|
|
389
|
+
}
|
|
390
|
+
|
|
75
391
|
export interface MutationRegistry {
|
|
76
392
|
/** optional suite command override for the whole registry (default `npm test`). */
|
|
77
393
|
readonly testCommand?: string;
|
package/src/operations.ts
CHANGED
|
@@ -1893,8 +1893,19 @@ export function probeHookLiveness(
|
|
|
1893
1893
|
command: string,
|
|
1894
1894
|
payload: string,
|
|
1895
1895
|
opts: { readonly cwd?: string; readonly env?: Readonly<Record<string, string>>; readonly timeoutMs?: number } = {},
|
|
1896
|
-
): { readonly status: number | null; readonly stdout: string; readonly stderr: string } {
|
|
1896
|
+
): { readonly status: number | null; readonly stdout: string; readonly stderr: string; readonly groupKillAttempted: boolean } {
|
|
1897
1897
|
const shell = process.env['SHELL'] ?? '/bin/sh';
|
|
1898
|
+
// Fix round 1 (apply-leg-never-silent, HIGH-1): a caller previously had to INFER "was the group
|
|
1899
|
+
// kill sent" by reading this function's source — a regression removing or bypassing the
|
|
1900
|
+
// `process.kill(-pid, ...)` call below would silently invalidate that inference. `groupKillAttempted`
|
|
1901
|
+
// is the OBSERVABLE fact instead: true exactly when this call reached the point of attempting the
|
|
1902
|
+
// kill syscall (`res.pid` was a real positive pid), false when it never got that far (e.g. the
|
|
1903
|
+
// spawn itself never produced a pid). It does NOT claim the signal found a live recipient — ESRCH
|
|
1904
|
+
// ("group already gone", the common successful-exit case) still counts as "sent": the syscall was
|
|
1905
|
+
// issued, its target simply no longer existed. That is a SEPARATE fact from whether the grandchild
|
|
1906
|
+
// is actually dead by the time a caller checks — see `probeApplyLeg`'s AM-5 test for the
|
|
1907
|
+
// kill-sent-vs-death-observed split this field exists to make possible.
|
|
1908
|
+
let groupKillAttempted = false;
|
|
1898
1909
|
try {
|
|
1899
1910
|
// AM-5 (fix round 1, apply-leg-never-silent): `detached: true` puts the shell in its OWN
|
|
1900
1911
|
// process GROUP (pgid === its own pid) instead of sharing the caller's — `spawnSync`'s own
|
|
@@ -1925,11 +1936,15 @@ export function probeHookLiveness(
|
|
|
1925
1936
|
process.kill(-res.pid, 'SIGKILL');
|
|
1926
1937
|
} catch {
|
|
1927
1938
|
/* group already gone */
|
|
1939
|
+
} finally {
|
|
1940
|
+
// set right after the process.kill(-pid, 'SIGKILL') attempt (HIGH-1 lead decision): reached
|
|
1941
|
+
// regardless of ESRCH, because ESRCH means "no recipient", not "syscall not issued".
|
|
1942
|
+
groupKillAttempted = true;
|
|
1928
1943
|
}
|
|
1929
1944
|
}
|
|
1930
|
-
return { status: res.status, stdout: res.stdout ?? '', stderr: res.stderr ?? '' };
|
|
1945
|
+
return { status: res.status, stdout: res.stdout ?? '', stderr: res.stderr ?? '', groupKillAttempted };
|
|
1931
1946
|
} catch (err) {
|
|
1932
|
-
return { status: null, stdout: '', stderr: String((err as Error)?.message ?? err) };
|
|
1947
|
+
return { status: null, stdout: '', stderr: String((err as Error)?.message ?? err), groupKillAttempted };
|
|
1933
1948
|
}
|
|
1934
1949
|
}
|
|
1935
1950
|
|