@dzhechkov/harness-core 0.4.5 → 0.5.1
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 +489 -105
- package/README.md +116 -5
- package/dist/agents-policy.d.ts +67 -0
- package/dist/agents-policy.d.ts.map +1 -0
- package/dist/agents-policy.js +258 -0
- package/dist/agents-policy.js.map +1 -0
- package/dist/codex-hooks-assets.d.ts +47 -0
- package/dist/codex-hooks-assets.d.ts.map +1 -0
- package/dist/codex-hooks-assets.js +324 -0
- package/dist/codex-hooks-assets.js.map +1 -0
- package/dist/codex-hooks-verify.d.ts +95 -0
- package/dist/codex-hooks-verify.d.ts.map +1 -0
- package/dist/codex-hooks-verify.js +169 -0
- package/dist/codex-hooks-verify.js.map +1 -0
- package/dist/codex-hooks.d.ts +341 -0
- package/dist/codex-hooks.d.ts.map +1 -0
- package/dist/codex-hooks.js +541 -0
- package/dist/codex-hooks.js.map +1 -0
- package/dist/discrimination-gate.d.ts +88 -15
- package/dist/discrimination-gate.d.ts.map +1 -1
- package/dist/discrimination-gate.js +343 -51
- package/dist/discrimination-gate.js.map +1 -1
- package/dist/feature-adr-checkpoints.d.ts +22 -0
- package/dist/feature-adr-checkpoints.d.ts.map +1 -1
- package/dist/feature-adr-checkpoints.js +42 -0
- package/dist/feature-adr-checkpoints.js.map +1 -1
- package/dist/feature-adr-routing.d.ts +218 -5
- package/dist/feature-adr-routing.d.ts.map +1 -1
- package/dist/feature-adr-routing.js +583 -54
- package/dist/feature-adr-routing.js.map +1 -1
- package/dist/guard.d.ts +13 -0
- package/dist/guard.d.ts.map +1 -1
- package/dist/guard.js +25 -1
- package/dist/guard.js.map +1 -1
- package/dist/index.d.ts +23 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +37 -4
- package/dist/index.js.map +1 -1
- package/dist/loop-blobs.generated.d.ts +1 -1
- package/dist/loop-blobs.generated.d.ts.map +1 -1
- package/dist/loop-blobs.generated.js +12 -3
- package/dist/loop-blobs.generated.js.map +1 -1
- package/dist/loop-plan.d.ts +70 -0
- package/dist/loop-plan.d.ts.map +1 -1
- package/dist/loop-plan.js +103 -0
- package/dist/loop-plan.js.map +1 -1
- package/dist/loop-render.d.ts.map +1 -1
- package/dist/loop-render.js +37 -129
- package/dist/loop-render.js.map +1 -1
- package/dist/loop-run-semantics.d.ts +130 -0
- package/dist/loop-run-semantics.d.ts.map +1 -0
- package/dist/loop-run-semantics.js +257 -0
- package/dist/loop-run-semantics.js.map +1 -0
- package/dist/loop-trace.d.ts +31 -2
- package/dist/loop-trace.d.ts.map +1 -1
- package/dist/loop-trace.js +89 -12
- package/dist/loop-trace.js.map +1 -1
- package/dist/managed-hooks.d.ts +86 -0
- package/dist/managed-hooks.d.ts.map +1 -0
- package/dist/managed-hooks.js +101 -0
- package/dist/managed-hooks.js.map +1 -0
- package/dist/mutation-gate.d.ts +14 -0
- package/dist/mutation-gate.d.ts.map +1 -1
- package/dist/mutation-gate.js +25 -2
- package/dist/mutation-gate.js.map +1 -1
- package/dist/named-lock.d.ts +57 -0
- package/dist/named-lock.d.ts.map +1 -0
- package/dist/named-lock.js +247 -0
- package/dist/named-lock.js.map +1 -0
- package/dist/operations.d.ts +229 -0
- package/dist/operations.d.ts.map +1 -1
- package/dist/operations.js +880 -26
- package/dist/operations.js.map +1 -1
- package/dist/parity.d.ts +106 -1
- package/dist/parity.d.ts.map +1 -1
- package/dist/parity.js +209 -5
- package/dist/parity.js.map +1 -1
- package/dist/qe-bridge.d.ts +291 -0
- package/dist/qe-bridge.d.ts.map +1 -0
- package/dist/qe-bridge.js +538 -0
- package/dist/qe-bridge.js.map +1 -0
- package/dist/recall-usage.d.ts +53 -0
- package/dist/recall-usage.d.ts.map +1 -1
- package/dist/recall-usage.js +125 -2
- package/dist/recall-usage.js.map +1 -1
- package/dist/score.d.ts.map +1 -1
- package/dist/score.js +43 -9
- package/dist/score.js.map +1 -1
- package/dist/setup.d.ts.map +1 -1
- package/dist/setup.js +14 -26
- package/dist/setup.js.map +1 -1
- package/dist/shell-veto-policy.d.ts +53 -0
- package/dist/shell-veto-policy.d.ts.map +1 -0
- package/dist/shell-veto-policy.js +103 -0
- package/dist/shell-veto-policy.js.map +1 -0
- package/dist/skills.d.ts +86 -1
- package/dist/skills.d.ts.map +1 -1
- package/dist/skills.js +116 -1
- package/dist/skills.js.map +1 -1
- package/dist/targets.d.ts +75 -0
- package/dist/targets.d.ts.map +1 -1
- package/dist/targets.js +160 -0
- package/dist/targets.js.map +1 -1
- package/dist/workflow-run-dispatch.d.ts +230 -0
- package/dist/workflow-run-dispatch.d.ts.map +1 -0
- package/dist/workflow-run-dispatch.js +363 -0
- package/dist/workflow-run-dispatch.js.map +1 -0
- package/dist/workflow-run.d.ts +498 -0
- package/dist/workflow-run.d.ts.map +1 -0
- package/dist/workflow-run.js +1359 -0
- package/dist/workflow-run.js.map +1 -0
- package/package.json +6 -5
- package/sbom.json +1099 -139
- package/src/agents-policy.ts +338 -0
- package/src/codex-hooks-assets.ts +328 -0
- package/src/codex-hooks-verify.ts +237 -0
- package/src/codex-hooks.ts +723 -0
- package/src/discrimination-gate.ts +456 -58
- package/src/feature-adr-checkpoints.ts +38 -0
- package/src/feature-adr-routing.ts +697 -75
- package/src/guard.ts +36 -1
- package/src/index.ts +162 -2
- package/src/loop-blobs.generated.ts +12 -3
- package/src/loop-plan.ts +185 -0
- package/src/loop-render.ts +37 -127
- package/src/loop-run-semantics.ts +278 -0
- package/src/loop-trace.ts +84 -10
- package/src/managed-hooks.ts +150 -0
- package/src/mutation-gate.ts +24 -2
- package/src/named-lock.ts +277 -0
- package/src/operations.ts +1121 -29
- package/src/parity.ts +295 -6
- package/src/qe-bridge.ts +737 -0
- package/src/recall-usage.ts +184 -1
- package/src/score.ts +50 -9
- package/src/setup.ts +26 -27
- package/src/shell-veto-policy.ts +119 -0
- package/src/skills.ts +174 -1
- package/src/targets.ts +189 -0
- package/src/workflow-run-dispatch.ts +459 -0
- package/src/workflow-run.ts +1743 -0
package/src/guard.ts
CHANGED
|
@@ -73,6 +73,19 @@ export interface GuardFacts {
|
|
|
73
73
|
readonly skillPacks?: readonly { readonly name: string; readonly nonRegistrable: readonly string[] }[];
|
|
74
74
|
/** for readme-first: per publishable package, is a version bump staged without a README change? */
|
|
75
75
|
readonly readmeFirst?: readonly { readonly name: string; readonly versionBumped: boolean; readonly readmeChanged: boolean }[];
|
|
76
|
+
/**
|
|
77
|
+
* for agents-md-policy-sync: result of the pure policy drift detector, gathered by the CLI.
|
|
78
|
+
* `applicable:false` is a repo whose canonical policy sources are unreadable; omission means the
|
|
79
|
+
* gatherer could not obtain evidence. `fenced` says whether the repo OPTED IN — i.e. its AGENTS.md
|
|
80
|
+
* already carries a `dz:policies` fence. The two combine to separate a repo that is out of scope
|
|
81
|
+
* (never opted in — silent) from one that opted in and then lost its sources (loud), so the shared
|
|
82
|
+
* `notes` channel does not carry a permanent "not applicable" line in every consumer repo.
|
|
83
|
+
*/
|
|
84
|
+
readonly policyDrift?: {
|
|
85
|
+
readonly applicable: boolean;
|
|
86
|
+
readonly drifted: readonly string[];
|
|
87
|
+
readonly fenced?: boolean;
|
|
88
|
+
};
|
|
76
89
|
/**
|
|
77
90
|
* for lockfile-in-sync: what each workspace package DECLARES vs what pnpm-lock.yaml RECORDS for that
|
|
78
91
|
* importer. `parsed:false` (or the fact absent) ⇒ the rule reports nothing — fail-open by construction,
|
|
@@ -227,6 +240,7 @@ export const DEFAULT_RULES: readonly GuardRule[] = [
|
|
|
227
240
|
{ id: 'readme-consistency', severity: 'soft', ops: ['publish'], description: 'README counts agree (CJM header vs All Commands, etc.)' },
|
|
228
241
|
{ id: 'skills-registrable', severity: 'soft', ops: ['publish'], description: 'every skill directory in a skill pack has a depth-1 SKILL.md (a buried or missing one ships un-registrable — the health-advisor 1.2.0 class)' },
|
|
229
242
|
{ id: 'readme-first', severity: 'soft', ops: ['publish'], description: 'a package with a staged version bump must update its own README.md in the same change (README-first)' },
|
|
243
|
+
{ id: 'agents-md-policy-sync', severity: 'soft', ops: ['publish'], description: 'proves the AGENTS.md copy is in SYNC with its source — not that the runtime read or obeyed it; heal drift with dz agents-sync' },
|
|
230
244
|
{ id: 'lockfile-in-sync', severity: 'soft', ops: ['publish'], description: 'every workspace @dzhechkov/* dependency spec matches the specifier pnpm-lock.yaml records for that importer (a dep bump without a lockfile refresh breaks CI with ERR_PNPM_OUTDATED_LOCKFILE). SOFT-ONLY — a config cannot promote it to HARD' },
|
|
231
245
|
{ id: 'store-bloat-cap', severity: 'soft', ops: ['teach', 'consolidate'], description: 'the learned store is within its size cap' },
|
|
232
246
|
// Description ASSEMBLED from STUB_MARKERS so guard.ts itself stays clean under the scan it defines
|
|
@@ -323,6 +337,16 @@ const CHECKERS: Record<string, (f: GuardFacts, sev: GuardSeverity) => Violation[
|
|
|
323
337
|
}
|
|
324
338
|
return out;
|
|
325
339
|
},
|
|
340
|
+
'agents-md-policy-sync': (f, _sev) => {
|
|
341
|
+
const fact = f.policyDrift;
|
|
342
|
+
if (!fact || fact.applicable !== true || !Array.isArray(fact.drifted)) return [];
|
|
343
|
+
const drifted = fact.drifted.filter((item) => typeof item === 'string');
|
|
344
|
+
return drifted.length === 0 ? [] : [{
|
|
345
|
+
rule: 'agents-md-policy-sync',
|
|
346
|
+
severity: 'soft',
|
|
347
|
+
detail: `${drifted.length} AGENTS.md policy section(s) are out of sync: ${drifted.slice(0, 8).join(', ')}${drifted.length > 8 ? '…' : ''} — heal with: dz agents-sync`,
|
|
348
|
+
}];
|
|
349
|
+
},
|
|
326
350
|
'lockfile-in-sync': (f, _sev) => {
|
|
327
351
|
// The 2026-07-28 CI break, mechanized: an overnight dep bump edited package.json and left
|
|
328
352
|
// pnpm-lock.yaml stale, so `pnpm install --frozen-lockfile` died with ERR_PNPM_OUTDATED_LOCKFILE.
|
|
@@ -417,7 +441,7 @@ const CHECKERS: Record<string, (f: GuardFacts, sev: GuardSeverity) => Violation[
|
|
|
417
441
|
* may not understand a file, and "I might be wrong" plus "block the publish" is the wrong pair. Disabling
|
|
418
442
|
* such a rule stays allowed — only the promotion is refused.
|
|
419
443
|
*/
|
|
420
|
-
export const SOFT_ONLY_RULES: readonly string[] = ['lockfile-in-sync'];
|
|
444
|
+
export const SOFT_ONLY_RULES: readonly string[] = ['lockfile-in-sync', 'agents-md-policy-sync'];
|
|
421
445
|
|
|
422
446
|
/**
|
|
423
447
|
* A well-formed PROMOTED rule: an id the engine does not know, made enforceable by a template +
|
|
@@ -533,6 +557,17 @@ export function evaluateGuard(facts: GuardFacts, rules: readonly GuardRule[] = D
|
|
|
533
557
|
notes.push(`no-stubs: ${skipped} changed scannable file(s) not scanned (deleted/oversize/unreadable/beyond the file cap) — the stub scan is fail-open, so this is a coverage gap on the record, not a violation`);
|
|
534
558
|
}
|
|
535
559
|
}
|
|
560
|
+
if (checked.includes('agents-md-policy-sync')) {
|
|
561
|
+
// A repo that never opted in (no `dz:policies` fence in AGENTS.md) is OUT OF SCOPE, not
|
|
562
|
+
// inconclusive — noting it on every run would put a permanent line in a channel that exists to
|
|
563
|
+
// flag genuine coverage gaps, and a note that is always there is a note nobody reads.
|
|
564
|
+
// Opted in but sources unreadable IS a gap, and stays loud.
|
|
565
|
+
if (facts.policyDrift === undefined) {
|
|
566
|
+
notes.push('agents-md-policy-sync: policy drift evidence was unavailable; the advisory rule skipped with a recorded coverage gap, not a silent pass');
|
|
567
|
+
} else if (facts.policyDrift.applicable !== true && facts.policyDrift.fenced === true) {
|
|
568
|
+
notes.push('agents-md-policy-sync: this repository carries a dz:policies fence but its canonical policy sources are unreadable — the advisory rule could not compare, and that gap is on the record, not a silent pass');
|
|
569
|
+
}
|
|
570
|
+
}
|
|
536
571
|
return { op, verdict, violations, checked, ...(notes.length > 0 ? { notes } : {}) };
|
|
537
572
|
}
|
|
538
573
|
|
package/src/index.ts
CHANGED
|
@@ -24,8 +24,13 @@ export {
|
|
|
24
24
|
GATE_FEATURE_IDS,
|
|
25
25
|
computeParity,
|
|
26
26
|
buildParityMatrix,
|
|
27
|
+
CAPABILITY_EVIDENCE,
|
|
28
|
+
findUnbackedCapabilities,
|
|
29
|
+
findStaleTranscriptEvidence,
|
|
30
|
+
newestRecordedRuntimeVersion,
|
|
31
|
+
downgradeForStaleEvidence,
|
|
27
32
|
} from './parity.js';
|
|
28
|
-
export type { RuntimeCapability, FeatureForm, ParityFeature, ParityCell, ParityMatrixRow } from './parity.js';
|
|
33
|
+
export type { RuntimeCapability, FeatureForm, ParityFeature, ParityCell, ParityReportCell, ParityMatrixRow, CapabilityEvidence, UnbackedCapability, ProbedRuntimeVersions } from './parity.js';
|
|
29
34
|
export * from './operations.js';
|
|
30
35
|
// workflows.ts: the ADR-005 templates are RETIRED (feature loop-designer, AM-6) — the module is a
|
|
31
36
|
// deprecation shim (empty WORKFLOW_NAMES). BREAKING for external harness-core consumers of
|
|
@@ -35,7 +40,13 @@ export * from './workflows.js';
|
|
|
35
40
|
// loop-designer (feature loop-designer): loop-plan/1 schema + generator + lint + trace planes.
|
|
36
41
|
export * from './loop-plan.js';
|
|
37
42
|
export * from './loop-plan-graph.js';
|
|
43
|
+
// loop-run-semantics: the ONE home of the enactment DECISIONS both enactors use — the generated
|
|
44
|
+
// Claude-host script (via the always-on `loop-semantics` blob) and `dz workflow run` (by import).
|
|
45
|
+
export * from './loop-run-semantics.js';
|
|
38
46
|
export * from './loop-render.js';
|
|
47
|
+
// dz workflow run (feature dz-workflow-run): the pure scheduler + the dispatcher seam.
|
|
48
|
+
export * from './workflow-run.js';
|
|
49
|
+
export * from './workflow-run-dispatch.js';
|
|
39
50
|
// loop-lint: EXPLICIT export list (QE round-2 G14) — `dominators`, the deliberately-WEAKER
|
|
40
51
|
// analysis kept in src/loop-lint.ts solely as AM-1's mutation seam, is NOT part of the published
|
|
41
52
|
// API surface; the in-package tests reach it via the module path directly.
|
|
@@ -76,6 +87,30 @@ export { pretrain } from './pretrain.js';
|
|
|
76
87
|
export { loadPatterns, loadSessions, computePatternBoost, readLearningConfig, readMemoryLearningConfig, BOOST_CAP, recordPattern, loadStorePatternsSync, loadStoreRecords, patternToRecord, recordToPattern, patternRecordId, patternIdentityOf, dreamRecordId, isMirrorableLearning, consolidateSessions, recallPatterns, pruneNoisePatterns, removePatternsByIds, snapshotStore, readReinforcementState, encodeReinforcementState, reinforcePattern, updateReinforcementState, storeStats, lessonDeltaReport, lessonDeltaMap, readQuarantineState, encodeQuarantineState, promotePatterns, quarantineExpiryCandidates, pruneQuarantinePatterns } from './patterns.js';
|
|
77
88
|
export { withStoreLock, withStoreLockSync, storeLockPath, StoreLockTimeoutError, StoreLockCompromisedError, STALE_LOCK_MS, LOCK_TIMEOUT_MS } from './store-lock.js';
|
|
78
89
|
export type { StoreLockOptions } from './store-lock.js';
|
|
90
|
+
export { withNamedLockSync, namedLockPath, isSafeLockName, NamedLockNameError, NamedLockTimeoutError, NamedLockCompromisedError } from './named-lock.js';
|
|
91
|
+
export {
|
|
92
|
+
QE_BRIDGE_SCHEMA,
|
|
93
|
+
QE_BRIDGE_FAILURE_SCHEMA,
|
|
94
|
+
CLAUDE_BRIDGE_PROMPT_CEILING_CHARS,
|
|
95
|
+
KNOWN_CLAUDE,
|
|
96
|
+
BRIDGE_MARKER,
|
|
97
|
+
BRIDGE_FENCE_LABEL,
|
|
98
|
+
BRIDGE_EXTRACT_END,
|
|
99
|
+
isSafeClaudeId,
|
|
100
|
+
CLAUDE_ISOLATION_ARGS,
|
|
101
|
+
BRIDGE_FAILURE_REASONS,
|
|
102
|
+
extractClaudeResult,
|
|
103
|
+
buildBridgeSignoffRecord,
|
|
104
|
+
claudeProbeArgs,
|
|
105
|
+
interpretClaudeProbe,
|
|
106
|
+
claudeReviewArgs,
|
|
107
|
+
defangSignoffEchoes,
|
|
108
|
+
buildBridgePrompt,
|
|
109
|
+
parseBridgeOutput,
|
|
110
|
+
buildBridgeFailureRecord,
|
|
111
|
+
renderBridgeReport,
|
|
112
|
+
} from './qe-bridge.js';
|
|
113
|
+
export type { BridgeFamily, BridgeFailureReason, BridgeFinding, BridgeSignoff, BridgeParse, BridgeParseOk, BridgeParseFail, BridgeChannels, BridgeAudit, ClaudeResultExtraction, NamedExtract, BridgePromptInput } from './qe-bridge.js';
|
|
79
114
|
export type { PatternRecord, SessionRecord, LearningConfig, MemoryLearningConfig, LoadOptions, ConsolidateResult, ConsolidateOptions, SqliteBackendMode, RecallHit, SessionsSource, PruneNoiseResult, RemovePatternsResult, SnapshotStoreResult, ReinforcementState, ReinforcePatternResult, StoreStats, LessonDeltaReport, LessonDeltaRow, QuarantineState, PromoteResult, QuarantineExpiryCandidate } from './patterns.js';
|
|
80
115
|
export { DEFAULT_REINFORCE_THRESHOLD, NoopLearningBackend, NativeReinforcementBackend, resolveLearningBackend, isLearningSignalBackend } from './learning-backend.js';
|
|
81
116
|
export type { LearningSignalBackend, LearningSignalStats, LearningSample, SignalCandidate, EnhanceContext, TrainingResult, LearningBackendMode } from './learning-backend.js';
|
|
@@ -196,6 +231,9 @@ export {
|
|
|
196
231
|
serializeTrainingPair,
|
|
197
232
|
trainingPairPath,
|
|
198
233
|
trainingPairAppendCmd,
|
|
234
|
+
// wave1-instrument-repair (ADR-003 Condition 3): the persist allowlist + composite shape check.
|
|
235
|
+
codeCheckpointPersistAllowed,
|
|
236
|
+
codeStageResultShapeValid,
|
|
199
237
|
} from './feature-adr-checkpoints.js';
|
|
200
238
|
export type { CheckpointStage, CheckpointEntry, ResumeMode, ResumeDecision, ParsedCheckpointRead, TrainingPairFamily, TrainingPair, TrainingPairEvaluation, TrainingPairProvenance, TrainingPairTruncation } from './feature-adr-checkpoints.js';
|
|
201
239
|
export {
|
|
@@ -257,6 +295,9 @@ export {
|
|
|
257
295
|
shouldCompactRecallUsageLogSize,
|
|
258
296
|
compactRecallUsageLog,
|
|
259
297
|
compactRecallUsageLogChecked,
|
|
298
|
+
appendRecallUsage,
|
|
299
|
+
runtimeOf,
|
|
300
|
+
RUNTIMES,
|
|
260
301
|
} from './recall-usage.js';
|
|
261
302
|
export type {
|
|
262
303
|
RecallUsageReadRecord,
|
|
@@ -271,7 +312,74 @@ export type {
|
|
|
271
312
|
CompactRecallUsageOptions,
|
|
272
313
|
CompactRecallUsageResult,
|
|
273
314
|
CompactRecallUsageStatus,
|
|
315
|
+
AppendRecallUsageInput,
|
|
316
|
+
AppendRecallUsageHit,
|
|
317
|
+
Runtime,
|
|
274
318
|
} from './recall-usage.js';
|
|
319
|
+
|
|
320
|
+
/* crossrt-2-codex-hooks (leg 2 of umbrella 98ed3967) */
|
|
321
|
+
export { mergeManagedHookEntries, hookCommandsOf } from './managed-hooks.js';
|
|
322
|
+
export type { HookMergePlan, MergeManagedHookOptions, ManagedHookEntry } from './managed-hooks.js';
|
|
323
|
+
export {
|
|
324
|
+
CODEX_MANAGED_HOOKS,
|
|
325
|
+
DZ_HOOK_HELPER_VERSION,
|
|
326
|
+
DZ_HOOK_TIMEOUT_SECONDS,
|
|
327
|
+
DZ_VETO_MATCHER,
|
|
328
|
+
buildHookCommand,
|
|
329
|
+
buildManagedEntries,
|
|
330
|
+
buildCodexHookManifest,
|
|
331
|
+
codexHooksPaths,
|
|
332
|
+
diffCodexHooks,
|
|
333
|
+
emitterWriteSet,
|
|
334
|
+
expectedTrustKey,
|
|
335
|
+
isDzManagedEntry,
|
|
336
|
+
isSafeForSingleQuote,
|
|
337
|
+
looksLikeDzEntry,
|
|
338
|
+
managedByEvent,
|
|
339
|
+
parseCodexRegistry,
|
|
340
|
+
parseCodexHookManifest,
|
|
341
|
+
planCodexHooks,
|
|
342
|
+
removeCodexHooks,
|
|
343
|
+
renderTrustBlock,
|
|
344
|
+
runtimeWriteSet,
|
|
345
|
+
selectOwnHookMetadata,
|
|
346
|
+
serializeCodexRegistry,
|
|
347
|
+
codexHookSha256,
|
|
348
|
+
trustEventName,
|
|
349
|
+
upsertTrustBlock,
|
|
350
|
+
} from './codex-hooks.js';
|
|
351
|
+
export type {
|
|
352
|
+
CodexHookEvent,
|
|
353
|
+
CodexHookId,
|
|
354
|
+
CodexHookManifest,
|
|
355
|
+
CodexHookManifestEntry,
|
|
356
|
+
CodexHooksPaths,
|
|
357
|
+
CodexHooksPlan,
|
|
358
|
+
CodexRegistry,
|
|
359
|
+
DriftReport,
|
|
360
|
+
HooksListHookMetadata,
|
|
361
|
+
HookTrustRow,
|
|
362
|
+
ManagedEntry,
|
|
363
|
+
} from './codex-hooks.js';
|
|
364
|
+
export {
|
|
365
|
+
DZ_VETO_MARKER,
|
|
366
|
+
DZ_VETO_WARN_MARKER,
|
|
367
|
+
RUNTIME_BLOCK_PHRASE,
|
|
368
|
+
classifyTrust,
|
|
369
|
+
classifyVetoProbe,
|
|
370
|
+
isReadyVerdict,
|
|
371
|
+
verifyExitCode,
|
|
372
|
+
} from './codex-hooks-verify.js';
|
|
373
|
+
export type {
|
|
374
|
+
CodexHookTrustStatus,
|
|
375
|
+
VetoProbeEvidence,
|
|
376
|
+
VetoProbeResult,
|
|
377
|
+
VetoTrust,
|
|
378
|
+
VetoVerdict,
|
|
379
|
+
} from './codex-hooks-verify.js';
|
|
380
|
+
export { SHELL_VETO_RULE_ID, resolveVetoMode, vetoShellCommand } from './shell-veto-policy.js';
|
|
381
|
+
export type { VetoHit, VetoMode } from './shell-veto-policy.js';
|
|
382
|
+
export { generateCodexHelpers, generateCodexRecallHelper, generateCodexVetoHelper } from './codex-hooks-assets.js';
|
|
275
383
|
export {
|
|
276
384
|
EVENT_CHAIN_SCOPE,
|
|
277
385
|
EVENT_CHAIN_GENESIS_HASH,
|
|
@@ -423,9 +531,58 @@ export {
|
|
|
423
531
|
needsCodeLandedBarrier,
|
|
424
532
|
codeLandedBarrierPlan,
|
|
425
533
|
codeLandedBarrierHasLanded,
|
|
534
|
+
// decideCodeLanding was NEVER in this named list (a pre-existing omission found by the ADR-002
|
|
535
|
+
// Confirmation-1 sweep). Its `predicate` union NARROWS in this release, which is a breaking change
|
|
536
|
+
// to a published output type — so it has to actually BE published to be honest about it.
|
|
537
|
+
decideCodeLanding,
|
|
426
538
|
CODE_LANDED_BARRIER_SLEEPS_SECONDS,
|
|
539
|
+
// wave1-instrument-repair (ADR-003): the landed-barrier honesty surface. This list is NAMED, so a
|
|
540
|
+
// missing entry silently drops the export from the published package — ADR-002 Confirmation 1 is
|
|
541
|
+
// the test that catches exactly that.
|
|
542
|
+
LANDING_PROTOCOL_VERSION,
|
|
543
|
+
LANDING_HASH_TOKEN,
|
|
544
|
+
addExpectedCodeTarget,
|
|
545
|
+
extractExpectedCodeTargetsFromText,
|
|
546
|
+
sourceExpectedCodeTargets,
|
|
547
|
+
validateExpectedTargetsBlock,
|
|
548
|
+
posixCksum,
|
|
549
|
+
verifyPreCodeBaseline,
|
|
550
|
+
preCodeBaselineCaptureCmd,
|
|
551
|
+
parseBaselineCapture,
|
|
552
|
+
CODE_LANDING_PREDICATES,
|
|
553
|
+
parseLandingSignal,
|
|
554
|
+
codeLandingProbeCmd,
|
|
555
|
+
// fa-plan-gate-wiring: the K2 plan-completeness gate halves. NAMED here on purpose — an omission
|
|
556
|
+
// silently drops them from the published package (the ADR-002 Confirmation-1 lesson).
|
|
557
|
+
PLAN_GATE_SCRIPT,
|
|
558
|
+
planCompletenessGateCmd,
|
|
559
|
+
parsePlanGateVerdict,
|
|
560
|
+
// qe-bridge-claude: the bridge's path/slug hygiene reuses these rather than minting a second
|
|
561
|
+
// definition of "safe" (ADR-001 D5-A).
|
|
562
|
+
isSafeSlug,
|
|
563
|
+
hasUnsafePathChars,
|
|
564
|
+
hasDotDotSegment,
|
|
565
|
+
} from './feature-adr-routing.js';
|
|
566
|
+
export type {
|
|
567
|
+
StageOpts,
|
|
568
|
+
RoutingEnv,
|
|
569
|
+
UsageSignal,
|
|
570
|
+
UsageAction,
|
|
571
|
+
CodeLandingStatus,
|
|
572
|
+
CodeStageLandingStatus,
|
|
573
|
+
LandingInconclusiveReason,
|
|
574
|
+
CodeLandedBarrierPlan,
|
|
575
|
+
CodeLandingSnapshot,
|
|
576
|
+
CodeLandingDecision,
|
|
577
|
+
SourcedCodeTargets,
|
|
578
|
+
ExpectedTargetRejectReason,
|
|
579
|
+
ExpectedTargetsValidation,
|
|
580
|
+
BaselineEntry,
|
|
581
|
+
BaselineVerification,
|
|
582
|
+
ParsedBaselineCapture,
|
|
583
|
+
ParsedLandingSignal,
|
|
584
|
+
PlanGateVerdict,
|
|
427
585
|
} from './feature-adr-routing.js';
|
|
428
|
-
export type { StageOpts, RoutingEnv, UsageSignal, UsageAction } from './feature-adr-routing.js';
|
|
429
586
|
export {
|
|
430
587
|
CLAUDE_USAGE_MODELS,
|
|
431
588
|
computeUsage,
|
|
@@ -497,6 +654,9 @@ export * from './model-recommender.js';
|
|
|
497
654
|
export * from './bto-optimize.js';
|
|
498
655
|
export * from './discrimination-gate.js';
|
|
499
656
|
export * from './guard.js';
|
|
657
|
+
// Root AGENTS.md policy projection: pure anchor extraction/render/hash drift +
|
|
658
|
+
// the shared-emitter I/O entry point exported from operations.
|
|
659
|
+
export * from './agents-policy.js';
|
|
500
660
|
// Lesson → guard-rule PROMOTION (feature guard-promotion, scout idea #1) — the cost-of-detection
|
|
501
661
|
// ladder's elevator: moves a lesson from layer 5 (agent memory) to layer 1 (a deterministic rule),
|
|
502
662
|
// but only after TWO consecutive shadow wins replayed over REAL commits. Never synthesises rule code.
|
|
@@ -25,7 +25,7 @@ export interface LoopBlob {
|
|
|
25
25
|
code: string;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
export const LOOP_BLOB_NAMES = ["checkpoints","training-pairs","model-resolver","usage-probes","codex-dispatch","challenge-panel","trace","ha-consult-router"] as const;
|
|
28
|
+
export const LOOP_BLOB_NAMES = ["checkpoints","training-pairs","model-resolver","usage-probes","codex-dispatch","challenge-panel","trace","loop-semantics","ha-consult-router"] as const;
|
|
29
29
|
|
|
30
30
|
/** Workflow files the regen-diff gate covers TODAY (AM-5 honest scope): exactly the files
|
|
31
31
|
* carrying BEGIN BLOB markers. Stage B (whole-file regeneration of feature-adr.js) is a
|
|
@@ -79,11 +79,11 @@ export const BLOBS: Record<string, LoopBlob> = {
|
|
|
79
79
|
"codex-dispatch": {
|
|
80
80
|
name: "codex-dispatch",
|
|
81
81
|
version: "1.0.0",
|
|
82
|
-
contentHash: "
|
|
82
|
+
contentHash: "140da3714d2507683b0a27fcb01fa449ea3cb1619f1a2e181aaf24291397642d",
|
|
83
83
|
sourcePath: "packages/@dzhechkov/harness-core/src/feature-adr-routing.ts",
|
|
84
84
|
requires: [],
|
|
85
85
|
exports: ["codexDispatchMode","codexExecPlan","needsCodeLandedBarrier","decideCodeLanding"],
|
|
86
|
-
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
|
|
86
|
+
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.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}",
|
|
87
87
|
},
|
|
88
88
|
"challenge-panel": {
|
|
89
89
|
name: "challenge-panel",
|
|
@@ -103,6 +103,15 @@ export const BLOBS: Record<string, LoopBlob> = {
|
|
|
103
103
|
exports: ["LOOP_TRACE_SCHEMA_VERSION","TRACE_RUNID_RE","TRACE_KEY_RE","traceShellQuote","traceValidateEvent","traceInit","traceOnDispatch","traceOnSettle","traceClose","traceFlushCmd","traceFaRecordCmd","traceLedgerLine","traceLedgerAppendCmd"],
|
|
104
104
|
code: "const LOOP_TRACE_SCHEMA_VERSION = 1;\nconst TRACE_RUNID_RE = /^[a-z0-9-]{1,40}$/;\nconst TRACE_KEY_RE = /^[a-z0-9_.:-]{1,64}$/i;\nfunction traceShellQuote(s) {\n return \"'\" + String(s).replace(/'/g, \"'\\\\''\") + \"'\";\n}\nfunction traceValidateEvent(e) {\n if (typeof e !== 'object' || e === null || Array.isArray(e))\n return 'event must be an object';\n const ev = e;\n if (ev['v'] !== 1)\n return 'v must be 1';\n if (typeof ev['runId'] !== 'string' || !TRACE_RUNID_RE.test(ev['runId']))\n return 'runId fails its VO regex';\n if (typeof ev['seq'] !== 'number' || !Number.isInteger(ev['seq']) || ev['seq'] < 1)\n return 'seq must be a positive integer';\n const kind = ev['event'];\n if (kind === 'dispatched') {\n if (typeof ev['invocationId'] !== 'string' || ev['invocationId'] === '')\n return 'invocationId required';\n if (typeof ev['stepId'] !== 'string' || !TRACE_KEY_RE.test(ev['stepId']))\n return 'stepId fails its VO regex';\n if (ev['itemKey'] !== null && (typeof ev['itemKey'] !== 'string' || !TRACE_KEY_RE.test(ev['itemKey'])))\n return 'itemKey fails its VO regex';\n if (typeof ev['attempt'] !== 'number' || ev['attempt'] < 1)\n return 'attempt must be >= 1';\n if (typeof ev['phase'] !== 'string' || ev['phase'] === '')\n return 'phase required';\n if (!Array.isArray(ev['causedBy']) || ev['causedBy'].some((n) => typeof n !== 'number'))\n return 'causedBy must be a number array';\n return null;\n }\n if (kind === 'settled') {\n if (typeof ev['invocationId'] !== 'string' || ev['invocationId'] === '')\n return 'invocationId required';\n if (ev['outcome'] !== 'ok' && ev['outcome'] !== 'null' && ev['outcome'] !== 'error')\n return 'outcome must be ok|null|error';\n return null;\n }\n if (kind === 'run.opened') {\n if (typeof ev['planDigest'] !== 'string' || typeof ev['execFp'] !== 'string')\n return 'run.opened needs planDigest + execFp';\n return null;\n }\n if (kind === 'run.closed') {\n const c = ev['counts'];\n if (typeof c !== 'object' || c === null)\n return 'run.closed needs counts';\n return null;\n }\n return 'unknown event kind';\n}\nfunction traceInit(runId, planDigest, execFp) {\n if (!TRACE_RUNID_RE.test(runId))\n throw new Error('loop-trace: runId fails ' + String(TRACE_RUNID_RE));\n const state = { runId, seq: 0, dispatched: 0, settled: 0, buffer: [] };\n const opened = { v: 1, runId, seq: ++state.seq, event: 'run.opened', planDigest, execFp };\n traceBuffer(state, opened);\n return state;\n}\nfunction traceBuffer(state, e) {\n const err = traceValidateEvent(e);\n if (err !== null)\n throw new Error('loop-trace: refusing non-conforming event (' + err + ') — the authoritative ordering source is never repaired later');\n state.buffer.push(JSON.stringify(e));\n}\nfunction traceOnDispatch(state, e) {\n const seq = ++state.seq;\n state.dispatched++;\n traceBuffer(state, {\n v: 1,\n runId: state.runId,\n seq,\n event: 'dispatched',\n invocationId: e.invocationId,\n stepId: e.stepId,\n itemKey: e.itemKey,\n attempt: e.attempt,\n phase: e.phase,\n model: e.model,\n causedBy: e.causedBy,\n });\n return seq;\n}\nfunction traceOnSettle(state, e) {\n const seq = ++state.seq;\n state.settled++;\n traceBuffer(state, { v: 1, runId: state.runId, seq, event: 'settled', invocationId: e.invocationId, outcome: e.outcome });\n return seq;\n}\nfunction traceClose(state) {\n const closed = {\n v: 1,\n runId: state.runId,\n seq: ++state.seq,\n event: 'run.closed',\n counts: { dispatched: state.dispatched, settled: state.settled },\n };\n traceBuffer(state, closed);\n}\nfunction traceFlushCmd(state, traceFileAbs) {\n if (state.buffer.length === 0)\n return null;\n const lines = state.buffer.splice(0, state.buffer.length);\n const file = traceShellQuote(traceFileAbs);\n const dir = traceShellQuote(traceFileAbs.replace(/\\/[^/]*$/, ''));\n const printfs = lines\n .map((l) => \"printf '%s\\\\n' \" + traceShellQuote(l) + ' | sed \"s/}$/,\\\\\"wallTime\\\\\":\\\\\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\\\\\"}/\" >> ' + file)\n .join(' && ');\n return 'mkdir -p ' + dir + ' && ' + printfs;\n}\nfunction traceFaRecordCmd(dzBin, slug, stepLabel, projectAbs) {\n if (typeof slug !== 'string' || slug === ''\n || typeof stepLabel !== 'string' || stepLabel === ''\n || typeof projectAbs !== 'string' || projectAbs === '')\n return null;\n const bin = typeof dzBin === 'string' && dzBin !== '' ? dzBin : 'dz';\n const cmd = traceShellQuote(bin) + ' statusline --fa-record --slug ' + traceShellQuote(slug)\n + ' --step ' + traceShellQuote(stepLabel) + ' --kind loop --project ' + traceShellQuote(projectAbs);\n return cmd + ' >/dev/null 2>&1';\n}\nfunction traceLedgerLine(opts) {\n try {\n if (typeof opts.slug !== 'string' || opts.slug === '')\n return null;\n const agents = typeof opts.agents === 'number'\n && Number.isFinite(opts.agents)\n && Number.isInteger(opts.agents)\n && opts.agents >= 0\n ? opts.agents\n : 0;\n const date = typeof opts.date === 'string' && /^\\d{4}-\\d{2}-\\d{2}$/.test(opts.date) ? opts.date : null;\n const outcome = typeof opts.outcome === 'string' && opts.outcome !== '' ? opts.outcome : 'unknown';\n const line = JSON.stringify({\n slug: opts.slug,\n stage: 'loop-run',\n tier: null,\n tokens: null,\n minutes: null,\n agents,\n coder: null,\n grade: null,\n date,\n auto: true,\n outcome,\n runId: typeof opts.runId === 'string' ? opts.runId : null,\n planDigest: typeof opts.planDigest === 'string' ? opts.planDigest : null,\n });\n return line.length <= 4000 ? line : null;\n }\n catch {\n return null;\n }\n}\nfunction traceLedgerAppendCmd(repoAbs, line) {\n if (typeof repoAbs !== 'string' || repoAbs === '' || typeof line !== 'string' || line === '')\n return null;\n const dir = traceShellQuote(repoAbs + '/.dz/feature-adr');\n const file = traceShellQuote(repoAbs + '/.dz/feature-adr/run-cost-ledger.jsonl');\n return 'mkdir -p ' + dir\n + \" && printf '%s' \" + traceShellQuote(line)\n + ' | sed \"s/\\\\\"date\\\\\":null/\\\\\"date\\\\\":\\\\\"$(date -u +%Y-%m-%d)\\\\\"/\" >> ' + file\n + \" && printf '\\\\n' >> \" + file\n + ' && echo LEDGER-OK';\n}\nfunction invocations(run) {\n const out = new Map();\n for (const e of run.events) {\n if (e.event === 'dispatched') {\n out.set(e.invocationId, {\n invocationId: e.invocationId,\n stepId: e.stepId,\n itemKey: e.itemKey,\n dispatchSeq: e.seq,\n settleSeq: null,\n causedBy: e.causedBy,\n });\n }\n else if (e.event === 'settled') {\n const inv = out.get(e.invocationId);\n if (inv)\n inv.settleSeq = e.seq;\n }\n }\n return [...out.values()];\n}",
|
|
105
105
|
},
|
|
106
|
+
"loop-semantics": {
|
|
107
|
+
name: "loop-semantics",
|
|
108
|
+
version: "1.0.0",
|
|
109
|
+
contentHash: "d918ba8d29d9dc20e2467ddb0ba126d537bc9e4560547e2ae52d28979ee6ea9c",
|
|
110
|
+
sourcePath: "packages/@dzhechkov/harness-core/src/loop-run-semantics.ts",
|
|
111
|
+
requires: [],
|
|
112
|
+
exports: ["errText","causeChain","errSnap","classifyFailure","gateVerdict","joinRegion"],
|
|
113
|
+
code: "function errText(err) {\n try {\n if (err !== null && typeof err === 'object') {\n const m = err.message;\n if (typeof m === 'string')\n return m;\n }\n return String(err);\n }\n catch (_e) {\n try {\n return Object.prototype.toString.call(err);\n }\n catch (_e2) {\n return '[unrenderable error]';\n }\n }\n}\nfunction causeChain(err) {\n const chain = [];\n let cur = err;\n for (let d = 0; d < 5; d++) {\n if (cur === null || cur === undefined)\n break;\n if (chain.indexOf(cur) !== -1)\n break;\n chain.push(cur);\n try {\n cur = typeof cur === 'object' ? cur.cause : undefined;\n }\n catch (_e) {\n cur = undefined;\n }\n }\n return chain.length > 0 ? chain : [err];\n}\nfunction errSnap(err) {\n const chain = causeChain(err);\n const snap = [];\n for (let ci = 0; ci < chain.length; ci++) {\n let code = null;\n try {\n const c = chain[ci] !== null && typeof chain[ci] === 'object' ? chain[ci].code : null;\n code = typeof c === 'string' ? c.toUpperCase() : null;\n }\n catch (_e) {\n code = null;\n }\n let name = null;\n try {\n const n = chain[ci] !== null && typeof chain[ci] === 'object' ? chain[ci].name : null;\n name = typeof n === 'string' ? n : null;\n }\n catch (_e) {\n name = null;\n }\n snap.push({ code: code, name: name, text: errText(chain[ci]) });\n }\n return snap;\n}\nfunction classifyFailure(outcome, snap) {\n if (outcome === 'null')\n return 'transport';\n const links = Array.isArray(snap) ? snap : [];\n for (let ci = 0; ci < links.length; ci++) {\n const code = links[ci].code;\n if (code === 'ETIMEDOUT' || code === 'ECONNRESET' || code === 'ECONNREFUSED' || code === 'ENOTFOUND' || code === 'EPIPE' || code === 'ECONNABORTED' || code === 'EAI_AGAIN')\n return 'transport';\n }\n for (let ci = 0; ci < links.length; ci++) {\n if (links[ci].name === 'SyntaxError')\n return 'malformed-output';\n }\n let msg = '';\n for (let ci = 0; ci < links.length; ci++)\n msg += (ci > 0 ? '\\n' : '') + links[ci].text;\n msg = msg.toLowerCase();\n if (/\\btransport\\b|\\beconnreset\\b|\\beconnrefused\\b|\\benotfound\\b|\\bepipe\\b|\\betimedout\\b|\\bsocket hang up\\b|\\bnetwork error\\b|\\brate[ -]?limit(ed|ing|s)?\\b|\\boverloaded\\b|\\bhttp 5[0-9][0-9]\\b/.test(msg))\n return 'transport';\n if (/\\bpolicy\\b|\\brefus(e|ed|es|al|ing)\\b|\\bdeclin(e|ed|es|ing)\\b|\\bcontent filter\\b|\\bsafety block\\b/.test(msg))\n return 'policy-refusal';\n if (/\\bmalformed\\b|\\bunparseable\\b|\\bparse error\\b|\\binvalid json\\b|\\bunexpected token\\b|\\bunexpected end of json\\b|\\bschema mismatch\\b/.test(msg))\n return 'malformed-output';\n if (/\\btimeout\\b|\\btimed out\\b/.test(msg))\n return 'timeout';\n return null;\n}\nfunction gateVerdict(reply) {\n if (typeof reply !== 'string')\n return 'invalid';\n const vLines = reply.split('\\n');\n const vRe = /^\\s*GATE:\\s*(PASS|FAIL)\\s*$/;\n let vCount = 0;\n let vLast = '';\n for (let i = 0; i < vLines.length; i++) {\n if (vRe.test(vLines[i]))\n vCount++;\n if (vLines[i].trim() !== '')\n vLast = vLines[i];\n }\n const vEnd = vRe.exec(vLast);\n if (vCount !== 1 || vEnd === null)\n return 'invalid';\n return vEnd[1] === 'PASS' ? 'pass' : 'fail';\n}\nfunction joinRegion(results, o) {\n const policy = o && o.policy ? o.policy : 'all-activated';\n const failures = [];\n for (let i = 0; i < results.length; i++) {\n if (results[i] === null || results[i] === undefined)\n failures.push(i);\n }\n if (policy === 'any') {\n if (failures.length === results.length)\n throw new Error('join ' + o.region + ': every branch failed (policy any)');\n return { ok: true, values: results, failures: failures };\n }\n const quorum = /^quorum:([1-9][0-9]*)$/.exec(policy);\n if (quorum) {\n const okN = results.length - failures.length;\n if (okN < Number(quorum[1]))\n throw new Error('join ' + o.region + ': quorum ' + quorum[1] + ' not met (' + okN + ' ok)');\n return { ok: true, values: results, failures: failures };\n }\n if (failures.length > 0)\n throw new Error('join ' + o.region + ': ' + failures.length + ' dispatched branch(es) failed under policy ' + policy + ' — a dispatched branch is never skippable');\n return { ok: true, values: results, failures: [] };\n}",
|
|
114
|
+
},
|
|
106
115
|
"ha-consult-router": {
|
|
107
116
|
name: "ha-consult-router",
|
|
108
117
|
version: "1.0.0",
|
package/src/loop-plan.ts
CHANGED
|
@@ -27,6 +27,10 @@
|
|
|
27
27
|
|
|
28
28
|
import { createHash } from 'node:crypto';
|
|
29
29
|
import { TRACE_KEY_RE } from './loop-trace.js';
|
|
30
|
+
// The ONE ceiling formula (ADR-004 Confirmation-2) — imported so the run projection's
|
|
31
|
+
// `budgetTotal` and the rendered script's `__budget` constant cannot drift into two ceilings.
|
|
32
|
+
// loop-run-semantics has no runtime import of its own, so this edge introduces no cycle.
|
|
33
|
+
import { computeBudgetTotal } from './loop-run-semantics.js';
|
|
30
34
|
|
|
31
35
|
export const LOOP_PLAN_SCHEMA = 'loop-plan/1';
|
|
32
36
|
|
|
@@ -1437,3 +1441,184 @@ export function toTraceProjection(plan: LoopPlan): TraceProjection {
|
|
|
1437
1441
|
expectedSteps: norm.steps.filter((s) => s.kind === 'agent' || s.kind === 'gate').map((s) => s.stepId),
|
|
1438
1442
|
};
|
|
1439
1443
|
}
|
|
1444
|
+
|
|
1445
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1446
|
+
// The FOURTH projection (feature dz-workflow-run, ADR-001 O1 + AM-3 doctrine).
|
|
1447
|
+
//
|
|
1448
|
+
// `dz workflow run` is a SECOND enactor of the same plan, and the AM-3 rule that kept the first
|
|
1449
|
+
// one honest applies to it unchanged: a consumer reads a PROJECTION, never `plan.steps`. The three
|
|
1450
|
+
// existing projections cannot serve it — `TraceProjection` is the invariant-checking view and
|
|
1451
|
+
// carries no prompts, models or budgets by design. So the scheduler gets its own view, added next
|
|
1452
|
+
// to the other three, and joins the same layer-1 consumer grep.
|
|
1453
|
+
//
|
|
1454
|
+
// The addition is ANALYTICAL: the `loop-plan/1` SCHEMA SURFACE (the LoopPlan/LoopStep types,
|
|
1455
|
+
// FIELD_DOMAINS, KNOWN_KEYS, REQUIRED_FIELDS, STEP_FIELD_KINDS, CROSS_REFS, parsePlan,
|
|
1456
|
+
// validatePlan, normalizePlan, planDigest) is byte-unchanged — nothing below can make a plan that
|
|
1457
|
+
// used to parse stop parsing, or change a digest.
|
|
1458
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1459
|
+
|
|
1460
|
+
/** One dispatching step as the scheduler needs it — everything it must know to build a prompt,
|
|
1461
|
+
* pick a model, spend budget, retry, and verify a deliverable landed. */
|
|
1462
|
+
export interface RunStepSpec {
|
|
1463
|
+
stepId: string;
|
|
1464
|
+
kind: 'agent' | 'gate';
|
|
1465
|
+
phase: string;
|
|
1466
|
+
/** USER prompt seed (`plan.steps[].prompt`); null means the plan declared none. */
|
|
1467
|
+
prompt: string | null;
|
|
1468
|
+
model: string | null;
|
|
1469
|
+
deliverable: Deliverable;
|
|
1470
|
+
reads: string[];
|
|
1471
|
+
writes: string[];
|
|
1472
|
+
tools: string[];
|
|
1473
|
+
/** INCLUDES the initial attempt (AM-4 semantics; 1 = no retry). */
|
|
1474
|
+
retryMaxAttempts: number;
|
|
1475
|
+
retryOn: FailureClass[];
|
|
1476
|
+
/** `budget.maxAgents ?? 1` — the per-step worst case a boundary reservation is built from. */
|
|
1477
|
+
maxAgents: number;
|
|
1478
|
+
/** `step['x-role'] === 'qe'` — the designed `x-` seam, not a schema field (fence: zero surgery). */
|
|
1479
|
+
qeRole: boolean;
|
|
1480
|
+
gate: { kind: string; failRoute: string | null; maxRedos: number } | null;
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
/** One SCHEDULING BOUNDARY: the unit the runner reserves budget for, checkpoints, and may pause
|
|
1484
|
+
* BEFORE. A parallel region is ONE boundary (its worst case is reserved as a whole), which is what
|
|
1485
|
+
* makes "pause before the region, never mid-region" expressible. */
|
|
1486
|
+
export interface RunBoundary {
|
|
1487
|
+
/** The stepId of the stage / fanout / gate / pause this boundary enacts. */
|
|
1488
|
+
boundaryId: string;
|
|
1489
|
+
kind: 'stage' | 'region' | 'gate' | 'pause';
|
|
1490
|
+
/** Happens-before inputs, as BOUNDARY ids: a declared dep on a join stage or a fanout member is
|
|
1491
|
+
* carried as its REGION, because the member and the join are not boundaries of their own. */
|
|
1492
|
+
deps: string[];
|
|
1493
|
+
/** Present for kind 'stage' | 'gate'. */
|
|
1494
|
+
stage?: RunStepSpec;
|
|
1495
|
+
/** Present for kind 'region'. */
|
|
1496
|
+
region?: {
|
|
1497
|
+
fanout: string;
|
|
1498
|
+
join: string;
|
|
1499
|
+
joinPolicy: string;
|
|
1500
|
+
onInvalid: string;
|
|
1501
|
+
maxFanout: number;
|
|
1502
|
+
registry: string[];
|
|
1503
|
+
dedup: boolean;
|
|
1504
|
+
shape: ConcurrencyShape;
|
|
1505
|
+
chain: RunStepSpec[];
|
|
1506
|
+
};
|
|
1507
|
+
/** Present for kind 'pause'. */
|
|
1508
|
+
pause?: { state: string; resumeArg: string; payloadSchema: Record<string, unknown> | null };
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
export interface RunProjection {
|
|
1512
|
+
kind: 'run-projection/1';
|
|
1513
|
+
/** Plan order — the render's enactment order, so both enactors walk the same sequence. */
|
|
1514
|
+
boundaries: RunBoundary[];
|
|
1515
|
+
/** `computeBudgetTotal(plan)` — the SAME number the rendered script spends against. */
|
|
1516
|
+
budgetTotal: number;
|
|
1517
|
+
/** Plan-declared `pauses[].resumeArg` keys — the enumerated argsHash exclusions (AM-13). */
|
|
1518
|
+
resumeArgKeys: string[];
|
|
1519
|
+
/** `plan.trace.emit === true` — the AM-6 gate input (a run with no trace plane is refused). */
|
|
1520
|
+
traceEmit: boolean;
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
/** The ONLY plan view `workflow-run.ts` consumes (AM-3, fourth consumer). */
|
|
1524
|
+
export function toRunProjection(plan: LoopPlan): RunProjection {
|
|
1525
|
+
const norm = normalizePlan(plan);
|
|
1526
|
+
const byId = new Map(norm.steps.map((s) => [s.stepId, s]));
|
|
1527
|
+
const gatesById = new Map((norm.gates ?? []).map((g) => [g.stepId, g]));
|
|
1528
|
+
const pausesByState = new Map((norm.pauses ?? []).map((p) => [p.state, p]));
|
|
1529
|
+
const fanouts = norm.fanouts ?? [];
|
|
1530
|
+
const joinsByForStage = new Map((norm.joins ?? []).map((j) => [j.forStage, j]));
|
|
1531
|
+
|
|
1532
|
+
/** member stepId → its owning fanout stage; join stepId → its owning fanout stage. */
|
|
1533
|
+
const ownedByRegion = new Map<string, string>();
|
|
1534
|
+
for (const f of fanouts) {
|
|
1535
|
+
for (const c of f.chain ?? []) ownedByRegion.set(c, f.stage);
|
|
1536
|
+
const j = joinsByForStage.get(f.stage);
|
|
1537
|
+
if (j !== undefined) ownedByRegion.set(j.stage, f.stage);
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
const specOf = (s: LoopStep): RunStepSpec => {
|
|
1541
|
+
const g = s.kind === 'gate' ? gatesById.get(s.stepId) : undefined;
|
|
1542
|
+
return {
|
|
1543
|
+
stepId: s.stepId,
|
|
1544
|
+
kind: s.kind === 'gate' ? 'gate' : 'agent',
|
|
1545
|
+
phase: s.phase,
|
|
1546
|
+
prompt: typeof s.prompt === 'string' ? s.prompt : null,
|
|
1547
|
+
model: typeof s.model === 'string' && s.model !== '' ? s.model : null,
|
|
1548
|
+
deliverable: s.deliverable ?? 'return-value',
|
|
1549
|
+
reads: [...(s.artifacts?.reads ?? [])],
|
|
1550
|
+
writes: [...(s.artifacts?.writes ?? [])],
|
|
1551
|
+
tools: [...(s.tools ?? [])],
|
|
1552
|
+
retryMaxAttempts: typeof s.retry?.maxAttempts === 'number' && s.retry.maxAttempts >= 1 ? Math.floor(s.retry.maxAttempts) : 1,
|
|
1553
|
+
retryOn: [...(s.retry?.retryableFailureClasses ?? [])],
|
|
1554
|
+
maxAgents: typeof s.budget?.maxAgents === 'number' ? s.budget.maxAgents : 1,
|
|
1555
|
+
qeRole: s['x-role'] === 'qe',
|
|
1556
|
+
gate:
|
|
1557
|
+
g === undefined
|
|
1558
|
+
? null
|
|
1559
|
+
: {
|
|
1560
|
+
kind: typeof g.kind === 'string' ? g.kind : 'gate',
|
|
1561
|
+
failRoute: typeof g.failRoute === 'string' ? g.failRoute : null,
|
|
1562
|
+
maxRedos: typeof g.maxRedos === 'number' && Number.isFinite(g.maxRedos) && g.maxRedos > 0 ? Math.floor(g.maxRedos) : 0,
|
|
1563
|
+
},
|
|
1564
|
+
};
|
|
1565
|
+
};
|
|
1566
|
+
|
|
1567
|
+
/** A declared dep resolved to the BOUNDARY that must have settled before this one dispatches. */
|
|
1568
|
+
const depBoundary = (dep: string): string => ownedByRegion.get(dep) ?? dep;
|
|
1569
|
+
|
|
1570
|
+
const boundaries: RunBoundary[] = [];
|
|
1571
|
+
for (const s of norm.steps) {
|
|
1572
|
+
if (ownedByRegion.has(s.stepId)) continue; // members and joins are rendered inside their region
|
|
1573
|
+
const deps = [...new Set((s.deps ?? []).map(depBoundary).filter((d) => d !== s.stepId))];
|
|
1574
|
+
if (s.kind === 'fanout') {
|
|
1575
|
+
const f = fanouts.find((x) => x.stage === s.stepId);
|
|
1576
|
+
const j = f === undefined ? undefined : joinsByForStage.get(f.stage);
|
|
1577
|
+
boundaries.push({
|
|
1578
|
+
boundaryId: s.stepId,
|
|
1579
|
+
kind: 'region',
|
|
1580
|
+
deps,
|
|
1581
|
+
region: {
|
|
1582
|
+
fanout: s.stepId,
|
|
1583
|
+
join: j?.stage ?? '',
|
|
1584
|
+
joinPolicy: j?.joinPolicy ?? '',
|
|
1585
|
+
onInvalid: j?.onInvalid ?? 'named-failure',
|
|
1586
|
+
maxFanout: typeof f?.maxFanout === 'number' ? f.maxFanout : 0,
|
|
1587
|
+
registry: [...(f?.registry ?? [])],
|
|
1588
|
+
dedup: f?.dedup === true,
|
|
1589
|
+
shape: s.concurrency ?? 'barrier',
|
|
1590
|
+
chain: (f?.chain ?? []).map((c) => byId.get(c)).filter((c): c is LoopStep => c !== undefined).map(specOf),
|
|
1591
|
+
},
|
|
1592
|
+
});
|
|
1593
|
+
continue;
|
|
1594
|
+
}
|
|
1595
|
+
if (s.kind === 'pause') {
|
|
1596
|
+
const state = typeof s.pauseState === 'string' ? s.pauseState : '';
|
|
1597
|
+
const declared = pausesByState.get(state);
|
|
1598
|
+
boundaries.push({
|
|
1599
|
+
boundaryId: s.stepId,
|
|
1600
|
+
kind: 'pause',
|
|
1601
|
+
deps,
|
|
1602
|
+
pause: {
|
|
1603
|
+
state,
|
|
1604
|
+
resumeArg: declared?.resumeArg ?? '',
|
|
1605
|
+
payloadSchema: declared?.payloadSchema ?? null,
|
|
1606
|
+
},
|
|
1607
|
+
});
|
|
1608
|
+
continue;
|
|
1609
|
+
}
|
|
1610
|
+
if (s.kind === 'gate') {
|
|
1611
|
+
boundaries.push({ boundaryId: s.stepId, kind: 'gate', deps, stage: specOf(s) });
|
|
1612
|
+
continue;
|
|
1613
|
+
}
|
|
1614
|
+
boundaries.push({ boundaryId: s.stepId, kind: 'stage', deps, stage: specOf(s) });
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
return {
|
|
1618
|
+
kind: 'run-projection/1',
|
|
1619
|
+
boundaries,
|
|
1620
|
+
budgetTotal: computeBudgetTotal(norm),
|
|
1621
|
+
resumeArgKeys: (norm.pauses ?? []).map((p) => p.resumeArg).filter((k): k is string => typeof k === 'string' && k !== ''),
|
|
1622
|
+
traceEmit: norm.trace?.emit === true,
|
|
1623
|
+
};
|
|
1624
|
+
}
|