harnery 0.37.0 → 0.38.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commander.d.ts +9 -0
- package/dist/commander.d.ts.map +1 -1
- package/dist/commander.js +7 -2
- package/dist/commands/admission.d.ts +21 -0
- package/dist/commands/admission.d.ts.map +1 -0
- package/dist/commands/admission.js +565 -0
- package/dist/commands/agents.d.ts +7 -0
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +61 -1
- package/dist/commands/artifacts.d.ts.map +1 -1
- package/dist/commands/artifacts.js +48 -2
- package/dist/commands/browse-ai.d.ts +2 -2
- package/dist/commands/browse-ai.d.ts.map +1 -1
- package/dist/commands/browse-ai.js +6 -4
- package/dist/commands/browse.d.ts.map +1 -1
- package/dist/commands/browse.js +349 -21
- package/dist/commands/fetch.js +1 -0
- package/dist/commands/qa-record.d.ts +144 -0
- package/dist/commands/qa-record.d.ts.map +1 -0
- package/dist/commands/qa-record.js +0 -0
- package/dist/commands/qa-run.d.ts +7 -3
- package/dist/commands/qa-run.d.ts.map +1 -1
- package/dist/commands/qa-run.js +268 -13
- package/dist/commands/qa-status.d.ts +71 -0
- package/dist/commands/qa-status.d.ts.map +1 -0
- package/dist/commands/qa-status.js +490 -0
- package/dist/commands/qa-verify.d.ts +40 -0
- package/dist/commands/qa-verify.d.ts.map +1 -0
- package/dist/commands/qa-verify.js +180 -0
- package/dist/commands/review-pack.d.ts +4 -0
- package/dist/commands/review-pack.d.ts.map +1 -0
- package/dist/commands/review-pack.js +1001 -0
- package/dist/core/agents/qa-signal.d.ts +111 -0
- package/dist/core/agents/qa-signal.d.ts.map +1 -0
- package/dist/core/agents/qa-signal.js +231 -0
- package/dist/core/agents/session-name-display.d.ts +20 -5
- package/dist/core/agents/session-name-display.d.ts.map +1 -1
- package/dist/core/agents/session-name-display.js +67 -7
- package/dist/core/agents/state/heartbeat-reader.d.ts +7 -0
- package/dist/core/agents/state/heartbeat-reader.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-writer.d.ts +11 -0
- package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-writer.js +17 -0
- package/dist/core/agents/state/live-coordination-view.d.ts.map +1 -1
- package/dist/core/agents/state/live-coordination-view.js +1 -0
- package/dist/core/agents/state/live-coordination-writer.js +5 -0
- package/dist/core/artifacts/constants.d.ts +1 -1
- package/dist/core/artifacts/constants.js +1 -1
- package/dist/core/artifacts/index.d.ts +57 -6
- package/dist/core/artifacts/index.d.ts.map +1 -1
- package/dist/core/artifacts/index.js +265 -10
- package/dist/core/config.d.ts +8 -0
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +16 -0
- package/dist/core/diagnostics/bundle.d.ts +16 -0
- package/dist/core/diagnostics/bundle.d.ts.map +1 -1
- package/dist/core/diagnostics/bundle.js +101 -7
- package/dist/core/events/v3/bootstrap.d.ts.map +1 -1
- package/dist/core/events/v3/bootstrap.js +10 -0
- package/dist/core/events/v3/coordination-view.d.ts +3 -0
- package/dist/core/events/v3/coordination-view.d.ts.map +1 -1
- package/dist/core/events/v3/coordination-view.js +69 -8
- package/dist/core/events/v3/producers/intake.d.ts.map +1 -1
- package/dist/core/events/v3/producers/intake.js +9 -2
- package/dist/core/events/v3/producers/recorder.d.ts +23 -0
- package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/recorder.js +264 -18
- package/dist/core/hooks/cli.js +120 -27
- package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
- package/dist/core/hooks/resolve/transcript.js +10 -3
- package/dist/core/hooks/session-name-presence.d.ts.map +1 -1
- package/dist/core/hooks/session-name-presence.js +4 -1
- package/dist/core/qa-artifacts.d.ts +20 -0
- package/dist/core/qa-artifacts.d.ts.map +1 -0
- package/dist/core/qa-artifacts.js +110 -0
- package/dist/core/resources/contract.d.ts +6 -0
- package/dist/core/resources/contract.d.ts.map +1 -1
- package/dist/core/resources/sampler.d.ts +7 -0
- package/dist/core/resources/sampler.d.ts.map +1 -1
- package/dist/core/resources/sampler.js +106 -5
- package/dist/lib/admission.d.ts +71 -0
- package/dist/lib/admission.d.ts.map +1 -0
- package/dist/lib/admission.js +264 -0
- package/dist/lib/agent-browser/client.d.ts +1 -1
- package/dist/lib/agent-browser/client.d.ts.map +1 -1
- package/dist/lib/agent-browser/client.js +1 -5
- package/dist/lib/browser/capture-fidelity.d.ts +39 -0
- package/dist/lib/browser/capture-fidelity.d.ts.map +1 -0
- package/dist/lib/browser/capture-fidelity.js +84 -0
- package/dist/lib/browser/client.d.ts +41 -1
- package/dist/lib/browser/client.d.ts.map +1 -1
- package/dist/lib/browser/client.js +167 -9
- package/dist/lib/browser/critique.d.ts +38 -1
- package/dist/lib/browser/critique.d.ts.map +1 -1
- package/dist/lib/browser/critique.js +34 -6
- package/dist/lib/browser/index.d.ts +4 -2
- package/dist/lib/browser/index.d.ts.map +1 -1
- package/dist/lib/browser/index.js +2 -0
- package/dist/lib/browser/page-review-judge.d.ts +64 -0
- package/dist/lib/browser/page-review-judge.d.ts.map +1 -0
- package/dist/lib/browser/page-review-judge.js +270 -0
- package/dist/lib/browser/page-review-pack.d.ts +613 -0
- package/dist/lib/browser/page-review-pack.d.ts.map +1 -0
- package/dist/lib/browser/page-review-pack.js +1751 -0
- package/dist/lib/browser/qa-run-contracts.d.ts +214 -10
- package/dist/lib/browser/qa-run-contracts.d.ts.map +1 -1
- package/dist/lib/browser/qa-run-contracts.js +136 -1
- package/dist/lib/browser/qa-run.d.ts +100 -10
- package/dist/lib/browser/qa-run.d.ts.map +1 -1
- package/dist/lib/browser/qa-run.js +768 -169
- package/dist/lib/browser/request-diagnostics.d.ts +13 -0
- package/dist/lib/browser/request-diagnostics.d.ts.map +1 -0
- package/dist/lib/browser/request-diagnostics.js +18 -0
- package/dist/lib/browser/tiling.d.ts +19 -0
- package/dist/lib/browser/tiling.d.ts.map +1 -1
- package/dist/lib/browser/tiling.js +28 -0
- package/dist/lib/cookies/client.d.ts +9 -0
- package/dist/lib/cookies/client.d.ts.map +1 -1
- package/dist/lib/cookies/client.js +197 -44
- package/dist/lib/cookies/extra.d.ts +18 -0
- package/dist/lib/cookies/extra.d.ts.map +1 -0
- package/dist/lib/cookies/extra.js +14 -0
- package/dist/lib/cookies/index.d.ts +2 -1
- package/dist/lib/cookies/index.d.ts.map +1 -1
- package/dist/lib/cookies/index.js +2 -1
- package/dist/lib/durable-job.d.ts +124 -0
- package/dist/lib/durable-job.d.ts.map +1 -0
- package/dist/lib/durable-job.js +296 -0
- package/dist/lib/http/client.d.ts +7 -1
- package/dist/lib/http/client.d.ts.map +1 -1
- package/dist/lib/http/client.js +2 -0
- package/dist/lib/instructions/templates.d.ts.map +1 -1
- package/dist/lib/instructions/templates.js +5 -2
- package/package.json +8 -2
- package/src/commander.ts +50 -2
- package/src/commands/admission.ts +699 -0
- package/src/commands/agents.ts +87 -1
- package/src/commands/artifacts.ts +97 -21
- package/src/commands/browse-ai.ts +10 -5
- package/src/commands/browse.ts +481 -20
- package/src/commands/fetch.ts +1 -0
- package/src/commands/qa-record.ts +682 -0
- package/src/commands/qa-run.ts +335 -16
- package/src/commands/qa-status.ts +608 -0
- package/src/commands/qa-verify.ts +238 -0
- package/src/commands/review-pack.ts +1281 -0
- package/src/core/agents/qa-signal.ts +261 -0
- package/src/core/agents/session-name-display.ts +78 -7
- package/src/core/agents/state/heartbeat-reader.ts +7 -0
- package/src/core/agents/state/heartbeat-writer.ts +23 -0
- package/src/core/agents/state/live-coordination-view.ts +1 -0
- package/src/core/agents/state/live-coordination-writer.ts +5 -0
- package/src/core/artifacts/constants.ts +1 -1
- package/src/core/artifacts/index.ts +370 -21
- package/src/core/config.ts +23 -0
- package/src/core/diagnostics/bundle.ts +119 -11
- package/src/core/events/v3/bootstrap.ts +10 -0
- package/src/core/events/v3/coordination-view.ts +100 -11
- package/src/core/events/v3/producers/intake.ts +9 -2
- package/src/core/events/v3/producers/recorder.ts +312 -18
- package/src/core/hooks/cli.ts +140 -32
- package/src/core/hooks/resolve/transcript.ts +10 -3
- package/src/core/hooks/session-name-presence.ts +6 -1
- package/src/core/qa-artifacts.ts +126 -0
- package/src/core/resources/contract.ts +7 -0
- package/src/core/resources/sampler.ts +138 -6
- package/src/lib/admission.ts +347 -0
- package/src/lib/agent-browser/client.ts +2 -10
- package/src/lib/browser/capture-fidelity.ts +98 -0
- package/src/lib/browser/client.ts +206 -10
- package/src/lib/browser/critique.ts +62 -7
- package/src/lib/browser/index.ts +36 -0
- package/src/lib/browser/page-review-judge.ts +360 -0
- package/src/lib/browser/page-review-pack.ts +2384 -0
- package/src/lib/browser/qa-run-contracts.ts +366 -3
- package/src/lib/browser/qa-run.ts +868 -190
- package/src/lib/browser/request-diagnostics.ts +27 -0
- package/src/lib/browser/tiling.ts +32 -0
- package/src/lib/cookies/client.ts +228 -42
- package/src/lib/cookies/extra.ts +28 -0
- package/src/lib/cookies/index.ts +2 -0
- package/src/lib/durable-job.ts +407 -0
- package/src/lib/http/client.ts +13 -1
- package/src/lib/instructions/templates.ts +5 -2
package/dist/commander.d.ts
CHANGED
|
@@ -96,6 +96,15 @@ export interface HarneryProgramContext {
|
|
|
96
96
|
* zones behind a WAF). harn standalone skips the callback entirely.
|
|
97
97
|
*/
|
|
98
98
|
extraHeaders?: (url: string) => Record<string, string>;
|
|
99
|
+
/**
|
|
100
|
+
* Optional callback that returns extra cookies to persist into the shared
|
|
101
|
+
* jar before `fetch` / `browse` attach it. Runs only when a jar is in
|
|
102
|
+
* play (`--no-cookies` skips it). Use this for host-minted session
|
|
103
|
+
* cookies; `extraHeaders` cannot merge a `Cookie` header once the jar
|
|
104
|
+
* has already set one. Must stay synchronous, same as `extraHeaders`.
|
|
105
|
+
* harn standalone skips the callback entirely.
|
|
106
|
+
*/
|
|
107
|
+
extraCookies?: import("./lib/cookies/index.js").ExtraCookies;
|
|
99
108
|
/**
|
|
100
109
|
* Host-injected vision-model call for `browse --check-critique`. harnery
|
|
101
110
|
* ships no model client or API key; a consumer wires this to its own
|
package/dist/commander.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"commander.d.ts","sourceRoot":"","sources":["../src/commander.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAKH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAQpC,OAAO,EACL,KAAK,iBAAiB,EACtB,KAAK,qBAAqB,EAC1B,mBAAmB,EACnB,eAAe,EACf,mBAAmB,EACnB,0BAA0B,GAC3B,MAAM,oBAAoB,CAAC;AAE5B,MAAM,WAAW,kBAAkB;IACjC;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB;;;;OAIG;IACH,OAAO,CAAC,EAAE,qBAAqB,CAAC;IAEhC;;;;;OAKG;IACH,IAAI,CAAC,EAAE,WAAW,CAAC;IAEnB;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAClC;AAED,8CAA8C;AAC9C,MAAM,WAAW,QAAQ;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,IAAI,GAAG,SAAS,GAAG,MAAM,GAAG,MAAM,CAAC;CAC7C;AAED;;;;GAIG;AACH,MAAM,MAAM,UAAU,GAAG,MAAM,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC;AAEnD,MAAM,WAAW,qBAAqB;IACpC,0FAA0F;IAC1F,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,qFAAqF;IACrF,gBAAgB,CAAC,EAAE,MAAM,MAAM,GAAG,IAAI,CAAC;IACvC;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC/B;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IACzC;;;;;;;OAOG;IACH,eAAe,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC;;;;;OAKG;IACH,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACvD;;;;;;OAMG;IACH,gBAAgB,CAAC,EAAE,OAAO,2BAA2B,EAAE,gBAAgB,CAAC;IACxE,0EAA0E;IAC1E,sBAAsB,CAAC,EAAE,MAAM,OAAO,CACpC,OAAO,2BAA2B,EAAE,gBAAgB,GAAG,SAAS,CACjE,CAAC;IACF;;;;;;OAMG;IACH,gBAAgB,CAAC,EAAE,CAAC,GAAG,EAAE;QACvB,WAAW,EAAE,MAAM,CAAC;QACpB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,KAAK,MAAM,GAAG,SAAS,CAAC;IACzB;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACvE;;;;;OAKG;IACH,yBAAyB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC9C;;;;;;;;OAQG;IACH,iBAAiB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACtC;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,4BAA4B,EAAE,8BAA8B,CAAC;IAC9E,qFAAqF;IACrF,2BAA2B,CAAC,EAAE,SAAS,OAAO,+BAA+B,EAAE,0BAA0B,EAAE,CAAC;IAC5G,mGAAmG;IACnG,iBAAiB,CAAC,EAAE,OAAO,yBAAyB,EAAE,mBAAmB,CAAC;IAC1E,iGAAiG;IACjG,aAAa,CAAC,EAAE;QACd,OAAO,EAAE,OAAO,iCAAiC,EAAE,0BAA0B,CAAC;QAC9E,cAAc,EAAE,MAAM,CAAC;QACvB,OAAO,CAAC,EAAE,MAAM,SAAS,OAAO,kCAAkC,EAAE,2BAA2B,EAAE,CAAC;KACnG,CAAC;IACF;;;;;;OAMG;IACH,kBAAkB,CAAC,EAAE,MAAM,GAAG,CAAC,MAAM,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;CACjE;AAED;;;;;;GAMG;AACH,MAAM,WAAW,WAAW;IAC1B,MAAM,CAAC,IAAI,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACxC,IAAI,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;IAC7B,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,IAAI,CAAC;IAC5C,IAAI,CAAC,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC3D,KAAK,CAAC,GAAG,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,KAAK,GAAG,OAAO,GAAG,IAAI,CAAC;IACrF,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,CAAC;IACpE,WAAW,CAAC,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AAED,eAAO,MAAM,WAAW,EAAE,WAkCzB,CAAC;AAEF,kFAAkF;AAClF,MAAM,WAAW,+BACf,SAAQ,IAAI,CAAC,kBAAkB,EAAE,MAAM,GAAG,SAAS,CAAC;IACpD,QAAQ,CAAC,EAAE,SAAS,CAAC,SAAS,GAAG,MAAM,CAAC,EAAE,CAAC;CAC5C;AAED,wBAAsB,iCAAiC,CACrD,MAAM,EAAE,OAAO,EACf,OAAO,GAAE,+BAAoC,GAC5C,OAAO,CAAC,OAAO,CAAC,CAYlB;AAED,wBAAgB,oBAAoB,CAAC,IAAI,GAAE,kBAAuB,GAAG,OAAO,CAiB3E"}
|
|
1
|
+
{"version":3,"file":"commander.d.ts","sourceRoot":"","sources":["../src/commander.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAKH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAQpC,OAAO,EACL,KAAK,iBAAiB,EACtB,KAAK,qBAAqB,EAC1B,mBAAmB,EACnB,eAAe,EACf,mBAAmB,EACnB,0BAA0B,GAC3B,MAAM,oBAAoB,CAAC;AAE5B,MAAM,WAAW,kBAAkB;IACjC;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB;;;;OAIG;IACH,OAAO,CAAC,EAAE,qBAAqB,CAAC;IAEhC;;;;;OAKG;IACH,IAAI,CAAC,EAAE,WAAW,CAAC;IAEnB;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAClC;AAED,8CAA8C;AAC9C,MAAM,WAAW,QAAQ;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,IAAI,GAAG,SAAS,GAAG,MAAM,GAAG,MAAM,CAAC;CAC7C;AAED;;;;GAIG;AACH,MAAM,MAAM,UAAU,GAAG,MAAM,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC;AAEnD,MAAM,WAAW,qBAAqB;IACpC,0FAA0F;IAC1F,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,qFAAqF;IACrF,gBAAgB,CAAC,EAAE,MAAM,MAAM,GAAG,IAAI,CAAC;IACvC;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC/B;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IACzC;;;;;;;OAOG;IACH,eAAe,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC;;;;;OAKG;IACH,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACvD;;;;;;;OAOG;IACH,YAAY,CAAC,EAAE,OAAO,wBAAwB,EAAE,YAAY,CAAC;IAC7D;;;;;;OAMG;IACH,gBAAgB,CAAC,EAAE,OAAO,2BAA2B,EAAE,gBAAgB,CAAC;IACxE,0EAA0E;IAC1E,sBAAsB,CAAC,EAAE,MAAM,OAAO,CACpC,OAAO,2BAA2B,EAAE,gBAAgB,GAAG,SAAS,CACjE,CAAC;IACF;;;;;;OAMG;IACH,gBAAgB,CAAC,EAAE,CAAC,GAAG,EAAE;QACvB,WAAW,EAAE,MAAM,CAAC;QACpB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,KAAK,MAAM,GAAG,SAAS,CAAC;IACzB;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACvE;;;;;OAKG;IACH,yBAAyB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC9C;;;;;;;;OAQG;IACH,iBAAiB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACtC;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,4BAA4B,EAAE,8BAA8B,CAAC;IAC9E,qFAAqF;IACrF,2BAA2B,CAAC,EAAE,SAAS,OAAO,+BAA+B,EAAE,0BAA0B,EAAE,CAAC;IAC5G,mGAAmG;IACnG,iBAAiB,CAAC,EAAE,OAAO,yBAAyB,EAAE,mBAAmB,CAAC;IAC1E,iGAAiG;IACjG,aAAa,CAAC,EAAE;QACd,OAAO,EAAE,OAAO,iCAAiC,EAAE,0BAA0B,CAAC;QAC9E,cAAc,EAAE,MAAM,CAAC;QACvB,OAAO,CAAC,EAAE,MAAM,SAAS,OAAO,kCAAkC,EAAE,2BAA2B,EAAE,CAAC;KACnG,CAAC;IACF;;;;;;OAMG;IACH,kBAAkB,CAAC,EAAE,MAAM,GAAG,CAAC,MAAM,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;CACjE;AAED;;;;;;GAMG;AACH,MAAM,WAAW,WAAW;IAC1B,MAAM,CAAC,IAAI,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACxC,IAAI,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;IAC7B,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,IAAI,CAAC;IAC5C,IAAI,CAAC,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC3D,KAAK,CAAC,GAAG,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,KAAK,GAAG,OAAO,GAAG,IAAI,CAAC;IACrF,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,CAAC;IACpE,WAAW,CAAC,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AAED,eAAO,MAAM,WAAW,EAAE,WAkCzB,CAAC;AAEF,kFAAkF;AAClF,MAAM,WAAW,+BACf,SAAQ,IAAI,CAAC,kBAAkB,EAAE,MAAM,GAAG,SAAS,CAAC;IACpD,QAAQ,CAAC,EAAE,SAAS,CAAC,SAAS,GAAG,MAAM,CAAC,EAAE,CAAC;CAC5C;AAED,wBAAsB,iCAAiC,CACrD,MAAM,EAAE,OAAO,EACf,OAAO,GAAE,+BAAoC,GAC5C,OAAO,CAAC,OAAO,CAAC,CAYlB;AAED,wBAAgB,oBAAoB,CAAC,IAAI,GAAE,kBAAuB,GAAG,OAAO,CAiB3E"}
|
package/dist/commander.js
CHANGED
|
@@ -105,9 +105,14 @@ function harneryCommandBundles({ emit, context, binName, }) {
|
|
|
105
105
|
lazy("files", "Mint browser links for files served by the local dashboard", async (program) => (await import("./commands/files.js")).registerFilesCommand(program, emit, context)),
|
|
106
106
|
lazy("read [html-file]", `Extract clean readable markdown from HTML. Reads from file or stdin (use '-'). Pair with \`${binName} fetch\` or \`${binName} browse\` for scrape-to-markdown.`, async (program) => (await import("./commands/read.js")).registerReadCommand(program, emit, binName), { hasOptions: true }),
|
|
107
107
|
lazy("browse <url>", "Headless Chromium with persistent profile + cookie jar. Default writes a trio of files (last.png, last.html, last.json) for the LLM iteration loop; --snapshot/--html/--json switch to stdout-print mode.", async (program) => (await import("./commands/browse.js")).registerBrowseCommand(program, emit, context, binName), { hasOptions: true }),
|
|
108
|
-
lazy("qa-run <target>", `Run the whole page-QA matrix in one command (planner, deterministic gates per context, interactions, critique, snapshot) via child \`${binName} browse\` processes; writes page-qa-result.json with a fail-closed verdict.`, async (program) => (await import("./commands/qa-run.js")).registerQaRunCommand(program, emit), { hasOptions: true }),
|
|
108
|
+
lazy("qa-run <target>", `Run the whole page-QA matrix in one command (planner, deterministic gates per context, interactions, critique, snapshot) via child \`${binName} browse\` processes; writes page-qa-result.json with a fail-closed verdict.`, async (program) => (await import("./commands/qa-run.js")).registerQaRunCommand(program, emit, context), { hasOptions: true }),
|
|
109
|
+
lazy("review-pack", "Page review packs: capture a page's rendering contexts (tiles, DOM, signature) into a directory an agent can review without a browser, and judge a pack from disk through one bounded pool of vision calls.", async (program) => (await import("./commands/review-pack.js")).registerReviewPackCommand(program, emit, context), { hasOptions: true }),
|
|
110
|
+
lazy("qa-verify <path>", "Verify a qa-run result is fresh evidence for a specific invocation: match its run identity (run_id, job digest, tested revision, age, recorded output directory) against expectations. Fail-closed on missing identity.", async (program) => (await import("./commands/qa-verify.js")).registerQaVerifyCommand(program, emit), { hasOptions: true }),
|
|
111
|
+
lazy("qa-record <target>", "Record hand-performed page QA as a standard result document: same schema, run identity, stage timings, and freshness guarantees as qa-run, marked evidence_source manual so it can report a defect but never a pass.", async (program) => (await import("./commands/qa-record.js")).registerQaRecordCommand(program, emit, context), { hasOptions: true }),
|
|
112
|
+
lazy("qa-status [path]", "Report a qa-run's live state (queued/launching/running/completed/dead) from its run-status.json, PID liveness, and result document; --wait reconnects to a detached run and exits with its verdict; --queue shows the machine-wide browser-qa admission queue.", async (program) => (await import("./commands/qa-status.js")).registerQaStatusCommand(program, emit, context), { hasOptions: true }),
|
|
113
|
+
lazy("admission", "Machine-wide admission control for heavy jobs: `admission run --resource <name> -- <command…>` waits for a FIFO slot then runs the command; `admission status` lists holders and waiters per resource.", async (program) => (await import("./commands/admission.js")).registerAdmissionCommand(program, emit)),
|
|
109
114
|
lazy("browse-session", "Control an opted-in headed browse session through an owner-only descriptor", async (program) => (await import("./commands/browse-session.js")).registerBrowseSessionCommand(program, emit)),
|
|
110
|
-
lazy("browse-ai <url>", `agent-browser (Vercel Labs) wrapper: accessibility-tree snapshots with element refs (@e1, @e2) for LLM consumption. Daemon-mode: successive calls reuse the same browser instance. Sister to ${binName} browse (Playwright).`, async (program) => (await import("./commands/browse-ai.js")).registerBrowseAiCommand(program, emit), { hasOptions: true }),
|
|
115
|
+
lazy("browse-ai <url>", `agent-browser (Vercel Labs) wrapper: accessibility-tree snapshots with element refs (@e1, @e2) for LLM consumption. Daemon-mode: successive calls reuse the same browser instance. Sister to ${binName} browse (Playwright).`, async (program) => (await import("./commands/browse-ai.js")).registerBrowseAiCommand(program, emit, context), { hasOptions: true }),
|
|
111
116
|
{
|
|
112
117
|
commands: [
|
|
113
118
|
{
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type Command } from "commander";
|
|
2
|
+
import type { EmitContext } from "../commander.js";
|
|
3
|
+
interface AdmissionSuperviseOpts {
|
|
4
|
+
jobDir: string;
|
|
5
|
+
timeout?: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Root for durable detached job records. A sibling of the admission queue root
|
|
9
|
+
* rather than a child of it, so job directories are never mistaken for
|
|
10
|
+
* admission resources by anything listing that directory.
|
|
11
|
+
*/
|
|
12
|
+
export declare function jobsBaseDir(): string;
|
|
13
|
+
export declare function registerAdmissionCommand(program: Command, emit: EmitContext): void;
|
|
14
|
+
/**
|
|
15
|
+
* Own one job record end to end: queue for the slot, run the command with its
|
|
16
|
+
* output on disk, then write the terminal record. Exported for tests, which
|
|
17
|
+
* drive it in-process against a temporary job directory.
|
|
18
|
+
*/
|
|
19
|
+
export declare function superviseJob(emit: EmitContext, opts: AdmissionSuperviseOpts): Promise<void>;
|
|
20
|
+
export {};
|
|
21
|
+
//# sourceMappingURL=admission.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"admission.d.ts","sourceRoot":"","sources":["../../src/commands/admission.ts"],"names":[],"mappings":"AAeA,OAAO,EAAE,KAAK,OAAO,EAAU,MAAM,WAAW,CAAC;AACjD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AA2CnD,UAAU,sBAAsB;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAiBD;;;;GAIG;AACH,wBAAgB,WAAW,IAAI,MAAM,CAEpC;AAiED,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,GAAG,IAAI,CA8TlF;AAkHD;;;;GAIG;AACH,wBAAsB,YAAY,CAAC,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,sBAAsB,GAAG,OAAO,CAAC,IAAI,CAAC,CA8GjG"}
|
|
@@ -0,0 +1,565 @@
|
|
|
1
|
+
// `admission`: inspect the machine-wide admission queues, or run an arbitrary
|
|
2
|
+
// command while holding a slot on a named resource. The queue mechanics live
|
|
3
|
+
// in src/lib/admission.ts and the durable job record in src/lib/durable-job.ts;
|
|
4
|
+
// this command owns flag parsing, human/JSON rendering, and exit-code
|
|
5
|
+
// propagation for the wrapped child process.
|
|
6
|
+
//
|
|
7
|
+
// `run --detach` splits the work in two: the client mints a job record and
|
|
8
|
+
// launches a detached supervisor, then returns. The supervisor holds the
|
|
9
|
+
// admission slot, runs the command, and writes the terminal record. Losing the
|
|
10
|
+
// client after that point interrupts nothing.
|
|
11
|
+
import { spawn } from "node:child_process";
|
|
12
|
+
import { randomUUID } from "node:crypto";
|
|
13
|
+
import { closeSync, openSync } from "node:fs";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { Option } from "commander";
|
|
16
|
+
import { resolveBinName } from "../core/config.js";
|
|
17
|
+
import { AdmissionTimeoutError, acquireAdmission, admissionBaseDir, admissionStatus, listAdmissionResources, } from "../lib/admission.js";
|
|
18
|
+
import { classifyJob, createJobDir, DURABLE_JOB_HEARTBEAT_MS, DURABLE_JOB_LOG_FILENAME, DURABLE_JOB_SCHEMA_VERSION, formatJobAge, jobExitCode, listJobs, readJobDocument, writeJobDocument, writeJobStatus, } from "../lib/durable-job.js";
|
|
19
|
+
import { coordEnv } from "../lib/env.js";
|
|
20
|
+
const LABEL_MAX_CHARS = 80;
|
|
21
|
+
const WAIT_POLL_MS = 2_000;
|
|
22
|
+
const DEFAULT_WAIT_TIMEOUT_MINUTES = 120;
|
|
23
|
+
const DEFAULT_JOBS_LIMIT = 20;
|
|
24
|
+
/**
|
|
25
|
+
* Root for durable detached job records. A sibling of the admission queue root
|
|
26
|
+
* rather than a child of it, so job directories are never mistaken for
|
|
27
|
+
* admission resources by anything listing that directory.
|
|
28
|
+
*/
|
|
29
|
+
export function jobsBaseDir() {
|
|
30
|
+
return coordEnv("JOBS_DIR") ?? `${admissionBaseDir()}-jobs`;
|
|
31
|
+
}
|
|
32
|
+
/** Parse an integer flag with a bounded range; undefined means invalid. */
|
|
33
|
+
function parseBoundedInt(raw, fallback, min, max) {
|
|
34
|
+
if (raw === undefined)
|
|
35
|
+
return fallback;
|
|
36
|
+
const value = Number(raw);
|
|
37
|
+
if (!Number.isInteger(value) || value < min || value > max)
|
|
38
|
+
return undefined;
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
function describeEntry(entry) {
|
|
42
|
+
const since = entry.acquired_at ?? entry.created_at;
|
|
43
|
+
return `${entry.label || "(no label)"} (pid ${entry.pid}) since ${since}`;
|
|
44
|
+
}
|
|
45
|
+
function logResourceStatus(emit, status) {
|
|
46
|
+
emit.log(`${status.resource}: ${status.holders.length} holder(s), ${status.waiters.length} waiter(s)`, "info");
|
|
47
|
+
for (const holder of status.holders)
|
|
48
|
+
emit.log(` holding: ${describeEntry(holder)}`, "info");
|
|
49
|
+
for (const waiter of status.waiters)
|
|
50
|
+
emit.log(` waiting: ${describeEntry(waiter)}`, "info");
|
|
51
|
+
}
|
|
52
|
+
function describeError(err) {
|
|
53
|
+
return err instanceof Error ? err.message : String(err);
|
|
54
|
+
}
|
|
55
|
+
function sleep(ms) {
|
|
56
|
+
return new Promise((resolvePromise) => setTimeout(resolvePromise, ms));
|
|
57
|
+
}
|
|
58
|
+
function nowIso() {
|
|
59
|
+
return new Date().toISOString();
|
|
60
|
+
}
|
|
61
|
+
/** One-line job summary shared by `jobs`, `wait`, and `status`. */
|
|
62
|
+
function describeJobReport(report, nowMs) {
|
|
63
|
+
const label = report.label || "(no label)";
|
|
64
|
+
const resource = report.resource ?? "(no resource)";
|
|
65
|
+
const bits = [`${report.state}`, `resource ${resource}`];
|
|
66
|
+
if (report.terminal) {
|
|
67
|
+
bits.push(report.signal !== null
|
|
68
|
+
? `killed by ${report.signal}`
|
|
69
|
+
: `exit ${report.exit_code ?? "unknown"}`);
|
|
70
|
+
}
|
|
71
|
+
else if (report.state === "dead") {
|
|
72
|
+
bits.push(`pid ${report.pid ?? "unknown"} not running`);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
bits.push(`pid ${report.pid ?? "unknown"}`);
|
|
76
|
+
}
|
|
77
|
+
if (report.heartbeat_age_ms !== null) {
|
|
78
|
+
bits.push(`heartbeat ${formatJobAge(report.heartbeat_age_ms)} ago`);
|
|
79
|
+
}
|
|
80
|
+
const startedMs = report.started_at !== null ? Date.parse(report.started_at) : Number.NaN;
|
|
81
|
+
if (!Number.isNaN(startedMs))
|
|
82
|
+
bits.push(`age ${formatJobAge(nowMs - startedMs)}`);
|
|
83
|
+
return `job ${report.job_id ?? "(no id)"}: ${bits.join(", ")} — ${label}`;
|
|
84
|
+
}
|
|
85
|
+
export function registerAdmissionCommand(program, emit) {
|
|
86
|
+
const admission = program
|
|
87
|
+
.command("admission")
|
|
88
|
+
.description("Machine-wide admission control for heavy jobs: inspect the per-resource " +
|
|
89
|
+
"slot queues, run a command while holding a slot, or launch it as a " +
|
|
90
|
+
"durable detached job that survives losing its client.")
|
|
91
|
+
.enablePositionalOptions();
|
|
92
|
+
// ------------------------------------------------------------------ status
|
|
93
|
+
admission
|
|
94
|
+
.command("status")
|
|
95
|
+
.description("Show holders and waiters per admission resource, plus any detached jobs " +
|
|
96
|
+
"still in flight. Queues prune dead-PID, expired, and torn entries as a " +
|
|
97
|
+
"side effect of being listed.")
|
|
98
|
+
.option("--resource <name>", "Show one resource instead of all of them.")
|
|
99
|
+
.option("--json", "Emit { resources: [...], jobs: [...] } as JSON.")
|
|
100
|
+
.addHelpText("after", "\nExit codes: 0 always (reporting only) · 1 usage error.")
|
|
101
|
+
.action((opts) => {
|
|
102
|
+
const dir = admissionBaseDir();
|
|
103
|
+
const resources = opts.resource !== undefined ? [opts.resource] : listAdmissionResources(dir);
|
|
104
|
+
const statuses = resources.map((resource) => admissionStatus({ dir, resource }));
|
|
105
|
+
const nowMs = Date.now();
|
|
106
|
+
const liveJobs = listJobs(jobsBaseDir())
|
|
107
|
+
.map((entry) => entry.report)
|
|
108
|
+
.filter((report) => report !== null && !report.terminal)
|
|
109
|
+
.filter((report) => report.state !== "dead" &&
|
|
110
|
+
(opts.resource === undefined || report.resource === opts.resource));
|
|
111
|
+
if (opts.json) {
|
|
112
|
+
emit.data({
|
|
113
|
+
resources: statuses.map((status) => ({
|
|
114
|
+
resource: status.resource,
|
|
115
|
+
holders: status.holders,
|
|
116
|
+
waiters: status.waiters,
|
|
117
|
+
})),
|
|
118
|
+
jobs: liveJobs,
|
|
119
|
+
});
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const active = statuses.filter((status) => status.holders.length > 0 || status.waiters.length > 0);
|
|
123
|
+
if (active.length === 0 && liveJobs.length === 0) {
|
|
124
|
+
emit.log(opts.resource !== undefined
|
|
125
|
+
? `no admission activity on ${opts.resource}`
|
|
126
|
+
: "no admission activity", "info");
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
for (const status of active)
|
|
130
|
+
logResourceStatus(emit, status);
|
|
131
|
+
if (liveJobs.length > 0) {
|
|
132
|
+
emit.log(`detached jobs in flight: ${liveJobs.length}`, "info");
|
|
133
|
+
for (const report of liveJobs)
|
|
134
|
+
emit.log(` ${describeJobReport(report, nowMs)}`, "info");
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
// --------------------------------------------------------------------- run
|
|
138
|
+
admission
|
|
139
|
+
.command("run")
|
|
140
|
+
.description("Acquire one slot on an admission resource, run <command...> with inherited " +
|
|
141
|
+
"stdio (no shell — the first token is the executable), then release the " +
|
|
142
|
+
"slot and propagate the child's exit code. Everything after -- reaches " +
|
|
143
|
+
"the child untouched. With --detach the command instead becomes a durable " +
|
|
144
|
+
"job supervised by a detached process.")
|
|
145
|
+
.passThroughOptions()
|
|
146
|
+
.requiredOption("--resource <name>", 'Admission resource to queue on, e.g. "browser-qa".')
|
|
147
|
+
.option("--capacity <n>", "Concurrent holders this machine should allow (1-32; default 1).")
|
|
148
|
+
.option("--timeout <minutes>", "Maximum admission wait before giving up (1-1440; default 60).")
|
|
149
|
+
.option("--label <text>", "Holder description shown in status listings (default: the command itself).")
|
|
150
|
+
.option("--detach", "Launch the command as a durable job: a detached supervisor holds the slot, " +
|
|
151
|
+
"runs the command, and records the outcome on disk. Prints the job id and " +
|
|
152
|
+
"job directory and returns immediately; losing the client no longer kills " +
|
|
153
|
+
"the job. Reconnect with admission wait <job-dir>.")
|
|
154
|
+
.option("--json", "With --detach, print the job envelope as JSON.")
|
|
155
|
+
.argument("<command...>", "Command to run while holding the slot.")
|
|
156
|
+
.addHelpText("after", "\nExit codes (foreground): the child's exit code · 4 admission timeout · " +
|
|
157
|
+
"1 usage error, spawn failure, or child killed by a signal." +
|
|
158
|
+
"\nExit codes (--detach): 0 once the job is launched · 1 usage error or " +
|
|
159
|
+
"launch failure. The job's own outcome comes from admission wait.")
|
|
160
|
+
.action(async (commandArgs, opts) => {
|
|
161
|
+
const capacity = parseBoundedInt(opts.capacity, 1, 1, 32);
|
|
162
|
+
if (capacity === undefined) {
|
|
163
|
+
emit.error({
|
|
164
|
+
code: "admission_usage",
|
|
165
|
+
message: "--capacity must be an integer between 1 and 32",
|
|
166
|
+
});
|
|
167
|
+
process.exitCode = 1;
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const timeoutMinutes = parseBoundedInt(opts.timeout, 60, 1, 1440);
|
|
171
|
+
if (timeoutMinutes === undefined) {
|
|
172
|
+
emit.error({
|
|
173
|
+
code: "admission_usage",
|
|
174
|
+
message: "--timeout must be an integer number of minutes between 1 and 1440",
|
|
175
|
+
});
|
|
176
|
+
process.exitCode = 1;
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const joined = commandArgs.join(" ");
|
|
180
|
+
const label = opts.label ??
|
|
181
|
+
(joined.length > LABEL_MAX_CHARS ? `${joined.slice(0, LABEL_MAX_CHARS - 3)}...` : joined);
|
|
182
|
+
if (opts.detach) {
|
|
183
|
+
launchDetachedJob(emit, {
|
|
184
|
+
commandArgs,
|
|
185
|
+
resource: opts.resource,
|
|
186
|
+
capacity,
|
|
187
|
+
label,
|
|
188
|
+
timeoutMinutes,
|
|
189
|
+
json: opts.json === true,
|
|
190
|
+
});
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const dir = admissionBaseDir();
|
|
194
|
+
let lastWaitMessage = "";
|
|
195
|
+
let handle;
|
|
196
|
+
try {
|
|
197
|
+
handle = await acquireAdmission({ dir, resource: opts.resource, capacity }, {
|
|
198
|
+
label,
|
|
199
|
+
timeoutMs: timeoutMinutes * 60_000,
|
|
200
|
+
onWait: (info) => {
|
|
201
|
+
const holders = info.holders.map((holder) => holder.label).join(", ") || "none";
|
|
202
|
+
const message = `queued for a ${opts.resource} slot: position ${info.position}, ` +
|
|
203
|
+
`capacity ${capacity}, holder(s): ${holders}`;
|
|
204
|
+
if (message !== lastWaitMessage) {
|
|
205
|
+
lastWaitMessage = message;
|
|
206
|
+
emit.log(message, "info");
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
catch (err) {
|
|
212
|
+
if (err instanceof AdmissionTimeoutError) {
|
|
213
|
+
emit.error({
|
|
214
|
+
code: "admission_timeout",
|
|
215
|
+
message: err.message,
|
|
216
|
+
hint: `${resolveBinName()} admission status --resource ${opts.resource} lists current holders`,
|
|
217
|
+
});
|
|
218
|
+
process.exitCode = 4;
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
throw err;
|
|
222
|
+
}
|
|
223
|
+
const [executable, ...childArgs] = commandArgs;
|
|
224
|
+
try {
|
|
225
|
+
const outcome = await new Promise((resolvePromise, rejectPromise) => {
|
|
226
|
+
const child = spawn(executable, childArgs, {
|
|
227
|
+
stdio: "inherit",
|
|
228
|
+
shell: false,
|
|
229
|
+
});
|
|
230
|
+
child.on("error", rejectPromise);
|
|
231
|
+
child.on("exit", (code, signal) => resolvePromise({ code, signal }));
|
|
232
|
+
});
|
|
233
|
+
if (outcome.signal !== null) {
|
|
234
|
+
emit.error({
|
|
235
|
+
code: "admission_child_signal",
|
|
236
|
+
message: `${executable} was terminated by signal ${outcome.signal}`,
|
|
237
|
+
});
|
|
238
|
+
process.exitCode = 1;
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
process.exitCode = outcome.code ?? 1;
|
|
242
|
+
}
|
|
243
|
+
catch (err) {
|
|
244
|
+
emit.error({
|
|
245
|
+
code: "admission_spawn_error",
|
|
246
|
+
message: `cannot run ${executable}: ${describeError(err)}`,
|
|
247
|
+
});
|
|
248
|
+
process.exitCode = 1;
|
|
249
|
+
}
|
|
250
|
+
finally {
|
|
251
|
+
handle.release();
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
// --------------------------------------------------------------- supervise
|
|
255
|
+
admission
|
|
256
|
+
.command("supervise", { hidden: true })
|
|
257
|
+
.description("Internal: run one durable job record to completion (detach child plumbing).")
|
|
258
|
+
.addOption(new Option("--job-dir <dir>", "Job directory to supervise.").makeOptionMandatory().hideHelp())
|
|
259
|
+
.addOption(new Option("--timeout <minutes>", "Maximum admission wait.").hideHelp())
|
|
260
|
+
.action(async (opts) => {
|
|
261
|
+
await superviseJob(emit, opts);
|
|
262
|
+
});
|
|
263
|
+
// -------------------------------------------------------------------- wait
|
|
264
|
+
admission
|
|
265
|
+
.command("wait <job-dir>")
|
|
266
|
+
.description("Reconnect to a detached job and block until it settles. Polls the job " +
|
|
267
|
+
"record every 2s: the record is authoritative, so a client that died and " +
|
|
268
|
+
"came back sees exactly the same outcome.")
|
|
269
|
+
.option("--timeout <minutes>", `Maximum time to wait for the job to settle (1-1440; default ${DEFAULT_WAIT_TIMEOUT_MINUTES}).`)
|
|
270
|
+
.option("--json", "Print the job report as JSON.")
|
|
271
|
+
.addHelpText("after", "\nExit codes: the job's own exit code once completed · 1 usage error or an " +
|
|
272
|
+
"unreadable job record · 4 the job is dead (non-terminal state, dead " +
|
|
273
|
+
"supervisor PID) · 5 the wait timed out while the job was still running.")
|
|
274
|
+
.action(async (jobDir, opts) => {
|
|
275
|
+
const timeoutMinutes = parseBoundedInt(opts.timeout, DEFAULT_WAIT_TIMEOUT_MINUTES, 1, 1440);
|
|
276
|
+
if (timeoutMinutes === undefined) {
|
|
277
|
+
emit.error({
|
|
278
|
+
code: "admission_usage",
|
|
279
|
+
message: "--timeout must be an integer number of minutes between 1 and 1440",
|
|
280
|
+
});
|
|
281
|
+
process.exitCode = 1;
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
const deadline = Date.now() + timeoutMinutes * 60_000;
|
|
285
|
+
let lastState = "";
|
|
286
|
+
while (true) {
|
|
287
|
+
const outcome = classifyJob(jobDir);
|
|
288
|
+
if (!outcome.ok) {
|
|
289
|
+
emit.error({ code: "admission_job_unreadable", message: outcome.error });
|
|
290
|
+
process.exitCode = 1;
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
const report = outcome.report;
|
|
294
|
+
const settled = report.terminal || report.state === "dead";
|
|
295
|
+
if (settled) {
|
|
296
|
+
if (opts.json)
|
|
297
|
+
emit.data(report);
|
|
298
|
+
emit.log(describeJobReport(report, Date.now()), report.terminal ? "info" : "warn");
|
|
299
|
+
if (!report.terminal)
|
|
300
|
+
emit.log(`log: ${report.log_path}`, "warn");
|
|
301
|
+
for (const warning of report.warnings)
|
|
302
|
+
emit.log(`warning: ${warning}`, "warn");
|
|
303
|
+
const exit = jobExitCode(report);
|
|
304
|
+
if (exit !== 0)
|
|
305
|
+
process.exitCode = exit;
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
// Progress prints on state transitions only; a job that runs for an
|
|
309
|
+
// hour should not scroll a line every two seconds.
|
|
310
|
+
if (report.state !== lastState) {
|
|
311
|
+
lastState = report.state;
|
|
312
|
+
emit.log(describeJobReport(report, Date.now()), "info");
|
|
313
|
+
}
|
|
314
|
+
if (Date.now() >= deadline) {
|
|
315
|
+
if (opts.json)
|
|
316
|
+
emit.data(report);
|
|
317
|
+
emit.log(`wait timed out after ${timeoutMinutes}m; job is still ${report.state}`, "warn");
|
|
318
|
+
process.exitCode = 5;
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
await sleep(WAIT_POLL_MS);
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
// -------------------------------------------------------------------- jobs
|
|
325
|
+
admission
|
|
326
|
+
.command("jobs")
|
|
327
|
+
.description("List recent detached jobs, newest first: id, resource, state, label, age, " +
|
|
328
|
+
"and exit code. A job whose supervisor PID is gone before it completed " +
|
|
329
|
+
"lists as dead.")
|
|
330
|
+
.option("--json", "Emit { dir, jobs: [...] } as JSON.")
|
|
331
|
+
.option("--limit <n>", `Maximum jobs to list (1-500; default ${DEFAULT_JOBS_LIMIT}).`)
|
|
332
|
+
.addHelpText("after", "\nExit codes: 0 always (reporting only) · 1 usage error.")
|
|
333
|
+
.action((opts) => {
|
|
334
|
+
const limit = parseBoundedInt(opts.limit, DEFAULT_JOBS_LIMIT, 1, 500);
|
|
335
|
+
if (limit === undefined) {
|
|
336
|
+
emit.error({
|
|
337
|
+
code: "admission_usage",
|
|
338
|
+
message: "--limit must be an integer between 1 and 500",
|
|
339
|
+
});
|
|
340
|
+
process.exitCode = 1;
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
const base = jobsBaseDir();
|
|
344
|
+
const entries = listJobs(base).slice(0, limit);
|
|
345
|
+
if (opts.json) {
|
|
346
|
+
emit.data({ dir: base, jobs: entries });
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
if (entries.length === 0) {
|
|
350
|
+
emit.log(`no detached jobs under ${base}`, "info");
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
const nowMs = Date.now();
|
|
354
|
+
for (const entry of entries) {
|
|
355
|
+
if (entry.report === null) {
|
|
356
|
+
emit.log(`job ${entry.job_id}: unreadable (${entry.error ?? "no status"})`, "warn");
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
emit.log(describeJobReport(entry.report, nowMs), entry.report.terminal ? "info" : "warn");
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
function launchDetachedJob(emit, options) {
|
|
364
|
+
const cliScript = process.argv[1];
|
|
365
|
+
if (!cliScript) {
|
|
366
|
+
emit.error({
|
|
367
|
+
code: "admission_no_cli_script",
|
|
368
|
+
message: "cannot resolve the host CLI script path to launch a detached supervisor",
|
|
369
|
+
});
|
|
370
|
+
process.exitCode = 1;
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
const jobId = randomUUID();
|
|
374
|
+
const base = jobsBaseDir();
|
|
375
|
+
let jobDir;
|
|
376
|
+
let logFd;
|
|
377
|
+
try {
|
|
378
|
+
jobDir = createJobDir(base, jobId);
|
|
379
|
+
const document = {
|
|
380
|
+
schema_version: DURABLE_JOB_SCHEMA_VERSION,
|
|
381
|
+
job_id: jobId,
|
|
382
|
+
resource: options.resource,
|
|
383
|
+
capacity: options.capacity,
|
|
384
|
+
label: options.label,
|
|
385
|
+
argv: options.commandArgs,
|
|
386
|
+
cwd: process.cwd(),
|
|
387
|
+
created_at: nowIso(),
|
|
388
|
+
};
|
|
389
|
+
writeJobDocument(jobDir, document);
|
|
390
|
+
logFd = openSync(join(jobDir, DURABLE_JOB_LOG_FILENAME), "a");
|
|
391
|
+
}
|
|
392
|
+
catch (err) {
|
|
393
|
+
emit.error({
|
|
394
|
+
code: "admission_job_setup_failed",
|
|
395
|
+
message: `cannot create a job record under ${base}: ${describeError(err)}`,
|
|
396
|
+
});
|
|
397
|
+
process.exitCode = 1;
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
// The supervisor is this same CLI, detached from the terminal with its
|
|
401
|
+
// output on disk: the client may die at any moment after this point.
|
|
402
|
+
let child;
|
|
403
|
+
try {
|
|
404
|
+
child = spawn(process.execPath, [
|
|
405
|
+
cliScript,
|
|
406
|
+
"admission",
|
|
407
|
+
"supervise",
|
|
408
|
+
"--job-dir",
|
|
409
|
+
jobDir,
|
|
410
|
+
"--timeout",
|
|
411
|
+
String(options.timeoutMinutes),
|
|
412
|
+
], { detached: true, stdio: ["ignore", logFd, logFd] });
|
|
413
|
+
child.unref();
|
|
414
|
+
}
|
|
415
|
+
catch (err) {
|
|
416
|
+
closeSync(logFd);
|
|
417
|
+
emit.error({
|
|
418
|
+
code: "admission_supervisor_spawn_failed",
|
|
419
|
+
message: `cannot launch the job supervisor: ${describeError(err)}`,
|
|
420
|
+
});
|
|
421
|
+
process.exitCode = 1;
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
closeSync(logFd);
|
|
425
|
+
const startedAt = nowIso();
|
|
426
|
+
const launching = {
|
|
427
|
+
schema_version: DURABLE_JOB_SCHEMA_VERSION,
|
|
428
|
+
job_id: jobId,
|
|
429
|
+
pid: child.pid ?? 0,
|
|
430
|
+
state: "launching",
|
|
431
|
+
started_at: startedAt,
|
|
432
|
+
updated_at: startedAt,
|
|
433
|
+
};
|
|
434
|
+
writeJobStatus(jobDir, launching);
|
|
435
|
+
const logPath = join(jobDir, DURABLE_JOB_LOG_FILENAME);
|
|
436
|
+
const binName = resolveBinName();
|
|
437
|
+
emit.log(`detached job ${jobId} (supervisor pid ${child.pid ?? "unknown"})`, "info");
|
|
438
|
+
emit.log(`job dir: ${jobDir}`, "info");
|
|
439
|
+
emit.log(`log: ${logPath}`, "info");
|
|
440
|
+
emit.log(`reconnect: ${binName} admission wait ${jobDir}`, "info");
|
|
441
|
+
if (options.json) {
|
|
442
|
+
emit.data({
|
|
443
|
+
detached: true,
|
|
444
|
+
job_id: jobId,
|
|
445
|
+
pid: child.pid ?? null,
|
|
446
|
+
job_dir: jobDir,
|
|
447
|
+
log: logPath,
|
|
448
|
+
resource: options.resource,
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
// ---------------------------------------------------------------------------
|
|
453
|
+
// Detach: supervisor half
|
|
454
|
+
// ---------------------------------------------------------------------------
|
|
455
|
+
/**
|
|
456
|
+
* Own one job record end to end: queue for the slot, run the command with its
|
|
457
|
+
* output on disk, then write the terminal record. Exported for tests, which
|
|
458
|
+
* drive it in-process against a temporary job directory.
|
|
459
|
+
*/
|
|
460
|
+
export async function superviseJob(emit, opts) {
|
|
461
|
+
const jobDir = opts.jobDir;
|
|
462
|
+
const document = readJobDocument(jobDir);
|
|
463
|
+
if (!document || document.argv.length === 0) {
|
|
464
|
+
emit.error({
|
|
465
|
+
code: "admission_job_document_unreadable",
|
|
466
|
+
message: `${jobDir} carries no usable job document`,
|
|
467
|
+
});
|
|
468
|
+
process.exitCode = 1;
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
const timeoutMinutes = parseBoundedInt(opts.timeout, 60, 1, 1440) ?? 60;
|
|
472
|
+
const startedAt = nowIso();
|
|
473
|
+
const status = {
|
|
474
|
+
schema_version: DURABLE_JOB_SCHEMA_VERSION,
|
|
475
|
+
job_id: document.job_id,
|
|
476
|
+
pid: process.pid,
|
|
477
|
+
state: "queued",
|
|
478
|
+
started_at: startedAt,
|
|
479
|
+
updated_at: startedAt,
|
|
480
|
+
queue: { resource: document.resource, waiting_since: startedAt },
|
|
481
|
+
};
|
|
482
|
+
writeJobStatus(jobDir, status);
|
|
483
|
+
const finish = (state) => {
|
|
484
|
+
writeJobStatus(jobDir, {
|
|
485
|
+
schema_version: DURABLE_JOB_SCHEMA_VERSION,
|
|
486
|
+
job_id: document.job_id,
|
|
487
|
+
pid: process.pid,
|
|
488
|
+
state: "completed",
|
|
489
|
+
started_at: startedAt,
|
|
490
|
+
updated_at: nowIso(),
|
|
491
|
+
exit_code: state.exitCode,
|
|
492
|
+
signal: state.signal,
|
|
493
|
+
});
|
|
494
|
+
};
|
|
495
|
+
let handle;
|
|
496
|
+
try {
|
|
497
|
+
handle = await acquireAdmission({
|
|
498
|
+
dir: admissionBaseDir(),
|
|
499
|
+
resource: document.resource,
|
|
500
|
+
capacity: document.capacity,
|
|
501
|
+
}, {
|
|
502
|
+
label: document.label,
|
|
503
|
+
timeoutMs: timeoutMinutes * 60_000,
|
|
504
|
+
onWait: () => {
|
|
505
|
+
writeJobStatus(jobDir, { ...status, updated_at: nowIso() });
|
|
506
|
+
},
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
catch (err) {
|
|
510
|
+
if (err instanceof AdmissionTimeoutError) {
|
|
511
|
+
emit.error({ code: "admission_timeout", message: err.message });
|
|
512
|
+
finish({ exitCode: 4, signal: null });
|
|
513
|
+
process.exitCode = 4;
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
finish({ exitCode: 1, signal: null });
|
|
517
|
+
throw err;
|
|
518
|
+
}
|
|
519
|
+
const runningAt = nowIso();
|
|
520
|
+
const running = {
|
|
521
|
+
schema_version: DURABLE_JOB_SCHEMA_VERSION,
|
|
522
|
+
job_id: document.job_id,
|
|
523
|
+
pid: process.pid,
|
|
524
|
+
state: "running",
|
|
525
|
+
started_at: startedAt,
|
|
526
|
+
updated_at: runningAt,
|
|
527
|
+
};
|
|
528
|
+
writeJobStatus(jobDir, running);
|
|
529
|
+
// The heartbeat is what lets a reader tell a wedged supervisor from a busy
|
|
530
|
+
// one; a dead supervisor stops writing and the record classifies as dead.
|
|
531
|
+
const heartbeat = setInterval(() => {
|
|
532
|
+
writeJobStatus(jobDir, { ...running, updated_at: nowIso() });
|
|
533
|
+
}, DURABLE_JOB_HEARTBEAT_MS);
|
|
534
|
+
heartbeat.unref?.();
|
|
535
|
+
const [executable, ...childArgs] = document.argv;
|
|
536
|
+
let logFd;
|
|
537
|
+
try {
|
|
538
|
+
logFd = openSync(join(jobDir, DURABLE_JOB_LOG_FILENAME), "a");
|
|
539
|
+
const outcome = await new Promise((resolvePromise, rejectPromise) => {
|
|
540
|
+
const child = spawn(executable, childArgs, {
|
|
541
|
+
stdio: ["ignore", logFd, logFd],
|
|
542
|
+
shell: false,
|
|
543
|
+
...(document.cwd ? { cwd: document.cwd } : {}),
|
|
544
|
+
});
|
|
545
|
+
child.on("error", rejectPromise);
|
|
546
|
+
child.on("exit", (code, signal) => resolvePromise({ code, signal }));
|
|
547
|
+
});
|
|
548
|
+
finish({ exitCode: outcome.code, signal: outcome.signal });
|
|
549
|
+
process.exitCode = outcome.signal !== null ? 1 : (outcome.code ?? 1);
|
|
550
|
+
}
|
|
551
|
+
catch (err) {
|
|
552
|
+
emit.error({
|
|
553
|
+
code: "admission_spawn_error",
|
|
554
|
+
message: `cannot run ${executable}: ${describeError(err)}`,
|
|
555
|
+
});
|
|
556
|
+
finish({ exitCode: 1, signal: null });
|
|
557
|
+
process.exitCode = 1;
|
|
558
|
+
}
|
|
559
|
+
finally {
|
|
560
|
+
clearInterval(heartbeat);
|
|
561
|
+
if (logFd !== undefined)
|
|
562
|
+
closeSync(logFd);
|
|
563
|
+
handle.release();
|
|
564
|
+
}
|
|
565
|
+
}
|
|
@@ -121,6 +121,13 @@ export type EventLedgerHealthV3 = {
|
|
|
121
121
|
/** Times the summary gate failed open (loose write proceeded). */
|
|
122
122
|
mitigation_fail_open: number;
|
|
123
123
|
};
|
|
124
|
+
coordination_authority: {
|
|
125
|
+
safe: boolean;
|
|
126
|
+
global_diagnostics: number;
|
|
127
|
+
isolated_diagnostics: number;
|
|
128
|
+
affected_generations: string[];
|
|
129
|
+
codes: string[];
|
|
130
|
+
};
|
|
124
131
|
span_pressure: Array<{
|
|
125
132
|
instance_id: string;
|
|
126
133
|
generation_id: string;
|