@brainervirus/workit-pi 6.0.0 → 7.0.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/workit.js CHANGED
@@ -3,9 +3,9 @@ import { randomUUID as randomUUID5 } from "node:crypto";
3
3
  import { fileURLToPath as fileURLToPath2 } from "node:url";
4
4
 
5
5
  // packages/workit-core/src/core.ts
6
- import { spawnSync as spawnSync10 } from "node:child_process";
7
- import { existsSync as existsSync6, realpathSync as realpathSync5 } from "node:fs";
8
- import path16 from "node:path";
6
+ import { spawnSync as spawnSync11 } from "node:child_process";
7
+ import { existsSync as existsSync7, realpathSync as realpathSync5 } from "node:fs";
8
+ import path19 from "node:path";
9
9
 
10
10
  // packages/workit-core/src/core/task-contract.ts
11
11
  import { createHash, randomUUID } from "node:crypto";
@@ -7429,6 +7429,14 @@ function object(shape, params) {
7429
7429
  };
7430
7430
  return new ZodObject(def);
7431
7431
  }
7432
+ function looseObject(shape, params) {
7433
+ return new ZodObject({
7434
+ type: "object",
7435
+ shape,
7436
+ catchall: unknown(),
7437
+ ...normalizeParams(params)
7438
+ });
7439
+ }
7432
7440
  var ZodUnion = /* @__PURE__ */ $constructor("ZodUnion", (inst, def) => {
7433
7441
  $ZodUnion.init(inst, def);
7434
7442
  ZodType.init(inst, def);
@@ -7929,76 +7937,16 @@ var provenanceSchema = object({
7929
7937
  workerId: nullableId
7930
7938
  }).strict();
7931
7939
  var entrySchema = (data) => object({ id, recordedAt: utc, provenance: provenanceSchema, data }).strict();
7932
- var factSchema = object({
7933
- statement: text,
7934
- basis: _enum(["observed", "inferred", "unknown"]),
7935
- refs: array(refSchema)
7936
- }).strict().check((ctx) => {
7937
- if (ctx.value.basis === "observed" && ctx.value.refs.length === 0)
7938
- ctx.issues.push({
7939
- code: "custom",
7940
- input: ctx.value,
7941
- message: "observed facts require supporting references",
7942
- path: ["refs"]
7943
- });
7944
- });
7945
- var signalSchema = object({
7946
- value: union([boolean2(), literal("unknown")]),
7947
- basis: _enum(["observed", "inferred", "unknown"]),
7948
- reason: text,
7940
+ var RISK_TIERS = ["trivial", "normal", "high"];
7941
+ var judgmentSchema = object({
7942
+ riskTier: _enum(RISK_TIERS),
7943
+ behaviorChange: boolean2(),
7944
+ productChoiceOpen: boolean2(),
7945
+ needsPlan: boolean2(),
7946
+ note: text.nullable(),
7949
7947
  refs: array(refSchema)
7950
- }).strict().check((ctx) => {
7951
- const signal = ctx.value;
7952
- const valid = signal.value === "unknown" ? signal.basis === "unknown" : signal.basis === "observed" || signal.basis === "inferred";
7953
- if (!valid)
7954
- ctx.issues.push({
7955
- code: "custom",
7956
- input: signal,
7957
- message: "signal value and basis disagree",
7958
- path: []
7959
- });
7960
- if (signal.basis === "observed" && signal.refs.length === 0)
7961
- ctx.issues.push({
7962
- code: "custom",
7963
- input: signal,
7964
- message: "observed signals require supporting references",
7965
- path: ["refs"]
7966
- });
7967
- });
7968
- var signalSet = object({
7969
- approachUnknown: signalSchema,
7970
- productChoiceOpen: signalSchema,
7971
- behaviorChange: signalSchema,
7972
- mechanicalLowRisk: signalSchema,
7973
- durableAgreementNeeded: signalSchema,
7974
- coordinationPlanNeeded: signalSchema,
7975
- helperUseful: signalSchema,
7976
- testFirstPractical: signalSchema
7977
- }).strict();
7978
- var consequenceSchema = object({
7979
- area: _enum([
7980
- "behavior",
7981
- "public_contract",
7982
- "data",
7983
- "security",
7984
- "operations",
7985
- "reversibility",
7986
- "consumers"
7987
- ]),
7988
- fact: factSchema
7989
- }).strict();
7990
- var verificationSchema = object({
7991
- claim: text,
7992
- scope: scopeSchema,
7993
- availableChecks: array(refSchema),
7994
- gaps: array(text)
7995
- }).strict();
7996
- var assessmentSchema = object({
7997
- facts: array(factSchema),
7998
- signals: signalSet,
7999
- consequences: array(consequenceSchema),
8000
- verification: array(verificationSchema)
8001
7948
  }).strict();
7949
+ var legacyAssessmentSchema = looseObject({});
8002
7950
  var constraintSchema = object({
8003
7951
  id: nonEmpty,
8004
7952
  kind: _enum(["host", "project", "user"]),
@@ -8235,7 +8183,8 @@ var taskRecordSchema = object({
8235
8183
  progress: progressSchema,
8236
8184
  pauseReason: text.nullable().optional(),
8237
8185
  runtime: runtimeSchema.optional(),
8238
- assessments: array(entrySchema(assessmentSchema)),
8186
+ assessments: array(entrySchema(legacyAssessmentSchema)),
8187
+ judgments: array(entrySchema(judgmentSchema)).optional(),
8239
8188
  policy: policySchema.nullable(),
8240
8189
  policyChanges: array(policyChangeSchema),
8241
8190
  candidates: array(candidateSchema),
@@ -8411,14 +8360,15 @@ var taskOperations = {
8411
8360
  decisionIds: array(id).optional()
8412
8361
  })
8413
8362
  };
8363
+ var rawJudgment = record(string2(), unknown());
8414
8364
  var policyOperations = {
8415
8365
  assess: operation({
8416
8366
  action: literal("assess"),
8417
8367
  ...taskId,
8418
8368
  expectedRevision: revision.optional(),
8419
- assessment: assessmentSchema
8369
+ judgment: rawJudgment
8420
8370
  }),
8421
- preview: operation({ action: literal("preview"), ...taskId, assessment: assessmentSchema }),
8371
+ preview: operation({ action: literal("preview"), ...taskId, judgment: rawJudgment }),
8422
8372
  explain: operation({ action: literal("explain"), ...taskId })
8423
8373
  };
8424
8374
  var evidenceOperations = {
@@ -8457,7 +8407,10 @@ var decisionOperations = {
8457
8407
  ...taskId,
8458
8408
  expectedRevision: revision.optional(),
8459
8409
  purpose: decisionSchema.shape.purpose,
8460
- binding: decisionSchema.shape.binding,
8410
+ binding: decisionSchema.shape.binding.extend({
8411
+ taskId: id.optional(),
8412
+ workspaceId: id.optional()
8413
+ }),
8461
8414
  response: decisionSchema.shape.response,
8462
8415
  requirementIds: array(digest)
8463
8416
  }),
@@ -8511,6 +8464,15 @@ var operationSchemas = {
8511
8464
  worker: discriminatedUnion("action", Object.values(workerOperations)),
8512
8465
  state: discriminatedUnion("action", Object.values(stateOperations))
8513
8466
  };
8467
+ var OPERATION_ACTIONS = {
8468
+ task: Object.keys(taskOperations),
8469
+ policy: Object.keys(policyOperations),
8470
+ evidence: Object.keys(evidenceOperations),
8471
+ finding: Object.keys(findingOperations),
8472
+ decision: Object.keys(decisionOperations),
8473
+ worker: Object.keys(workerOperations),
8474
+ state: Object.keys(stateOperations)
8475
+ };
8514
8476
  var withImplicitTask = (options) => Object.values(options).map((option) => ("taskId" in option.shape) ? option.extend({ taskId: id.optional() }) : option);
8515
8477
  var advertised = (options) => discriminatedUnion("action", withImplicitTask(options));
8516
8478
  var advertisedOperationSchemas = {
@@ -8544,103 +8506,6 @@ function parseOperation(family, input) {
8544
8506
  }));
8545
8507
  return failure2("invalid_input", "operation input is invalid", { fields });
8546
8508
  }
8547
- function operationJsonSchema(family) {
8548
- return toJSONSchema(advertisedOperationSchemas[family], { target: "draft-2020-12" });
8549
- }
8550
- var OPERATION_SCHEMA_DEPTH = 1;
8551
- var schemaShapeDescription = (value, depth = 0) => {
8552
- if (!value || typeof value !== "object")
8553
- return "value";
8554
- const schema = value;
8555
- if (Array.isArray(schema.oneOf) || Array.isArray(schema.anyOf)) {
8556
- const options = Array.isArray(schema.oneOf) ? schema.oneOf : schema.anyOf;
8557
- return `(${options.map((option) => schemaShapeDescription(option, depth)).join("|")})`;
8558
- }
8559
- if (schema.const !== undefined)
8560
- return `=${JSON.stringify(schema.const)}`;
8561
- if (Array.isArray(schema.enum))
8562
- return `[${schema.enum.map(String).join("|")}]`;
8563
- const type = Array.isArray(schema.type) ? schema.type.join("/") : schema.type;
8564
- if (type === "array")
8565
- return `arr<${schemaShapeDescription(schema.items, depth)}>`;
8566
- if (type === "object" && schema.properties && typeof schema.properties === "object") {
8567
- if (depth >= 2)
8568
- return "obj";
8569
- const required = new Set(Array.isArray(schema.required) ? schema.required : []);
8570
- const properties = schema.properties;
8571
- const fields = Object.entries(properties).map(([name, child]) => `${name}${required.has(name) ? "!" : ""}:${schemaShapeDescription(child, depth + 1)}`);
8572
- return `obj{${fields.join(",")}}`;
8573
- }
8574
- if (typeof type !== "string")
8575
- return "?";
8576
- return type.split("/").map((part) => ({ string: "str", boolean: "bool", number: "num", null: "null" })[part] ?? part).join("|");
8577
- };
8578
- var canonicalFieldsDescription = (value) => {
8579
- if (Array.isArray(value))
8580
- return `Canonical object fields: ${value.join(", ")}. Workit validates the complete nested value.`;
8581
- const isZodType = (schema) => ("_zod" in schema) && typeof schema._zod === "object";
8582
- const schema = isZodType(value) ? toJSONSchema(value, { target: "draft-2020-12" }) : value;
8583
- if (!schema.properties || typeof schema.properties !== "object") {
8584
- const variants = Array.isArray(schema.oneOf) ? schema.oneOf : Array.isArray(schema.anyOf) ? schema.anyOf : null;
8585
- return variants ? `Canonical shapes: ${variants.map((variant) => schemaShapeDescription(variant)).join(" | ")}. Full nested value is validated.` : "Canonical nested value; Workit validates the complete value.";
8586
- }
8587
- const properties = schema.properties;
8588
- const required = new Set(Array.isArray(schema.required) ? schema.required : []);
8589
- const fields = Object.entries(properties).map(([name, child]) => `${name}${required.has(name) ? "!" : ""}:${schemaShapeDescription(child)}`);
8590
- return `Fields (! required): ${fields.join(", ")}. Full nested value is validated.`;
8591
- };
8592
- function boundedOperationJsonSchema(family, maxDepth = OPERATION_SCHEMA_DEPTH, stringifiedObjects = false) {
8593
- const tolerate = (projected) => {
8594
- if (!stringifiedObjects)
8595
- return projected;
8596
- if (projected.properties && typeof projected.properties === "object")
8597
- return { ...projected, type: ["object", "string"] };
8598
- if (projected.items)
8599
- return { ...projected, type: ["array", "string"] };
8600
- if ("const" in projected && (typeof projected.const === "number" || typeof projected.const === "boolean"))
8601
- return { anyOf: [projected, { const: String(projected.const) }] };
8602
- if (projected.type === "object")
8603
- return { ...projected, type: ["object", "string"] };
8604
- if (projected.type === "array")
8605
- return { ...projected, type: ["array", "string"] };
8606
- return projected;
8607
- };
8608
- const collapse = (node, currentDepth) => {
8609
- if (Array.isArray(node))
8610
- return node.map((item) => collapse(item, currentDepth));
8611
- if (typeof node !== "object" || node === null)
8612
- return node;
8613
- const record = node;
8614
- if (record.properties && typeof record.properties === "object") {
8615
- const properties = record.properties;
8616
- const fields = Object.keys(properties);
8617
- if (currentDepth >= maxDepth || fields.length === 0) {
8618
- const description = stringifiedObjects ? `${canonicalFieldsDescription(record)} A JSON-encoded string is also accepted.` : canonicalFieldsDescription(record);
8619
- return tolerate({ type: "object", description });
8620
- }
8621
- return tolerate({
8622
- ...record,
8623
- properties: Object.fromEntries(Object.entries(record.properties).map(([key, child]) => [
8624
- key,
8625
- collapse(child, currentDepth + 1)
8626
- ]))
8627
- });
8628
- }
8629
- if (record.items)
8630
- return tolerate({ ...record, items: collapse(record.items, currentDepth) });
8631
- const composed = ["anyOf", "oneOf", "allOf"].filter((key) => Array.isArray(record[key]));
8632
- if (composed.length > 0) {
8633
- const projected = { ...record };
8634
- for (const key of composed)
8635
- projected[key] = record[key].map((item) => collapse(item, currentDepth));
8636
- return projected;
8637
- }
8638
- if ("const" in record)
8639
- return tolerate({ ...record });
8640
- return node;
8641
- };
8642
- return collapse(operationJsonSchema(family), 0);
8643
- }
8644
8509
  function canonical(value, seen) {
8645
8510
  if (value === null || typeof value === "boolean" || typeof value === "string") {
8646
8511
  if (typeof value === "string" && invalidUnicode(value))
@@ -8729,6 +8594,402 @@ function requirementId(input) {
8729
8594
  }
8730
8595
  });
8731
8596
  }
8597
+ // packages/workit-core/src/core/operation-input.ts
8598
+ var str = (d, values) => ({
8599
+ type: "string",
8600
+ ...values ? { enum: values } : {},
8601
+ ...d ? { d } : {}
8602
+ });
8603
+ var list = (d) => ({ type: "array", ...d ? { d } : {} });
8604
+ var bool = (d) => ({ type: "boolean", ...d ? { d } : {} });
8605
+ var int2 = (d) => ({ type: "integer", ...d ? { d } : {} });
8606
+ var scopeFields = {
8607
+ paths: list("Scope paths (default: whole checkout)."),
8608
+ exclusions: list(),
8609
+ scope: str("Scope description.")
8610
+ };
8611
+ var refs = list("Paths or URLs.");
8612
+ var FLAT = {
8613
+ task: {
8614
+ fields: {
8615
+ objective: str(),
8616
+ ...scopeFields,
8617
+ refs,
8618
+ reason: str(),
8619
+ summary: str(),
8620
+ nextAction: str(),
8621
+ blockers: list(),
8622
+ outcome: str(undefined, ["verified", "accepted_limitations", "stopped"]),
8623
+ decisionIds: list(),
8624
+ status: str(undefined, ["open", "closed", "all"]),
8625
+ query: str(),
8626
+ limit: int2(),
8627
+ view: str(undefined, ["summary", "full"])
8628
+ }
8629
+ },
8630
+ policy: {
8631
+ fields: {
8632
+ riskTier: str("low=trivial, medium=normal.", ["trivial", "normal", "high", "low", "medium"]),
8633
+ behaviorChange: bool("Observable behavior changes."),
8634
+ productChoiceOpen: bool("A product/preference choice is still open."),
8635
+ needsPlan: bool("Needs a written plan or spec first."),
8636
+ note: str(),
8637
+ refs: list("Plan/spec paths or URLs.")
8638
+ }
8639
+ },
8640
+ evidence: {
8641
+ fields: {
8642
+ kind: str(undefined, ["check", "review", "investigation", "artifact"]),
8643
+ claim: str(),
8644
+ result: str(undefined, ["passed", "failed", "missing", "skipped"]),
8645
+ summary: str(),
8646
+ exitCode: int2(),
8647
+ refs,
8648
+ requirementIds: list(),
8649
+ candidateId: str(),
8650
+ beforeCandidateId: str(),
8651
+ reviewSession: str("Reviewer session handle (review evidence).")
8652
+ }
8653
+ },
8654
+ finding: {
8655
+ fields: {
8656
+ claim: str(),
8657
+ consequence: str(),
8658
+ ...scopeFields,
8659
+ candidateId: str(),
8660
+ refs,
8661
+ findingId: str(),
8662
+ disposition: str(undefined, ["open", "fixed", "dismissed", "deferred"]),
8663
+ reason: str(),
8664
+ evidenceIds: list(),
8665
+ decisionIds: list()
8666
+ }
8667
+ },
8668
+ decision: {
8669
+ fields: {
8670
+ purpose: str(undefined, ["design", "action", "limitation", "preference"]),
8671
+ response: str(undefined, ["approved", "rejected", "stated"]),
8672
+ presented: str("What the user was asked."),
8673
+ choice: str("The user's stated choice (response=stated)."),
8674
+ ...scopeFields,
8675
+ refs,
8676
+ requirementIds: list(),
8677
+ decisionId: str(),
8678
+ reason: str()
8679
+ }
8680
+ },
8681
+ worker: {
8682
+ fields: {
8683
+ role: str(undefined, ["investigator", "reviewer", "implementer"]),
8684
+ objective: str(),
8685
+ ...scopeFields,
8686
+ decisionIds: list(),
8687
+ requirementIds: list(),
8688
+ candidateId: str(),
8689
+ stoppingCondition: str(),
8690
+ workerId: str(),
8691
+ outcome: str(undefined, ["completed", "failed", "cancelled"]),
8692
+ summary: str(),
8693
+ evidenceIds: list(),
8694
+ findingIds: list(),
8695
+ reason: str()
8696
+ }
8697
+ },
8698
+ state: {
8699
+ fields: { bundle: { type: "object", d: "An export bundle (object or JSON string)." } }
8700
+ }
8701
+ };
8702
+ var COMMON = {
8703
+ taskId: str("Omit for this branch's task."),
8704
+ expectedRevision: str(),
8705
+ expectedWorkspaceRevision: str()
8706
+ };
8707
+ var DESCRIPTIONS = {
8708
+ task: "Workit task lifecycle for this branch's task (optional continuity).",
8709
+ policy: "Judge the work in four flat calls (riskTier, behaviorChange, productChoiceOpen, needsPlan); Workit derives the checks, verdicts, decisions and plan it needs.",
8710
+ evidence: "Record a note-level evidence entry; only `workit check` runs satisfy check gates.",
8711
+ finding: "Record or resolve a review finding.",
8712
+ decision: "Record or revoke a user decision (never an authorization).",
8713
+ worker: "Assign, report or cancel a bounded helper.",
8714
+ state: "Export or import a task handoff bundle."
8715
+ };
8716
+ var operationDescription = (family) => `${DESCRIPTIONS[family]} Flat fields; omit taskId and revisions.`;
8717
+ var READ_TASK_FIELDS = new Set(["status", "query", "limit", "view"]);
8718
+ var READ_ONLY_ACTIONS = new Set(["list", "inspect", "preview", "explain", "export"]);
8719
+ function flatOperationJsonSchema(family, options = {}) {
8720
+ const spec = FLAT[family];
8721
+ const actions = options.readOnly ? OPERATION_ACTIONS[family].filter((action) => READ_ONLY_ACTIONS.has(action)) : [...OPERATION_ACTIONS[family]];
8722
+ if (!actions.length)
8723
+ return null;
8724
+ const property = (field) => {
8725
+ const base = field.type === "array" ? { type: "array", items: { type: "string" } } : field.type === "object" ? { type: "object" } : { type: field.type };
8726
+ const tolerant = options.stringTolerant && field.type !== "string" ? { type: [field.type, "string"] } : {};
8727
+ return {
8728
+ ...base,
8729
+ ...tolerant,
8730
+ ...field.enum ? { enum: field.enum } : {},
8731
+ ...field.d ? { description: field.d } : {}
8732
+ };
8733
+ };
8734
+ const properties = { action: { type: "string", enum: actions } };
8735
+ const readField = (name) => name === "taskId" || (family === "task" ? READ_TASK_FIELDS.has(name) : family === "policy");
8736
+ for (const [name, field] of Object.entries({ ...COMMON, ...spec.fields }))
8737
+ if (options.readOnly ? readField(name) : !(family === "state" && name === "expectedRevision"))
8738
+ properties[name] = property(field);
8739
+ return {
8740
+ $schema: "https://json-schema.org/draft/2020-12/schema",
8741
+ type: "object",
8742
+ properties,
8743
+ required: ["action"]
8744
+ };
8745
+ }
8746
+ var isObject2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
8747
+ var decoded = (value) => {
8748
+ if (typeof value !== "string")
8749
+ return value;
8750
+ const trimmed = value.trim();
8751
+ if (!/^[[{]/.test(trimmed) && !/^(true|false|null|-?\d+)$/.test(trimmed))
8752
+ return value;
8753
+ try {
8754
+ return JSON.parse(trimmed);
8755
+ } catch {
8756
+ return value;
8757
+ }
8758
+ };
8759
+ var strings = (value) => {
8760
+ const plain = decoded(value);
8761
+ if (typeof plain === "string")
8762
+ return plain.trim() ? [plain] : [];
8763
+ return plain;
8764
+ };
8765
+ var refOf = (value) => {
8766
+ if (typeof value !== "string")
8767
+ return value;
8768
+ return /^https?:\/\//i.test(value) ? { kind: "external", url: value } : { kind: "file", path: value.replace(/^\.\//, ""), digest: null };
8769
+ };
8770
+ var refList = (value) => {
8771
+ const items = strings(value);
8772
+ return Array.isArray(items) ? items.map(refOf) : items;
8773
+ };
8774
+ var take = (flat, keys, map = decoded) => {
8775
+ const out = {};
8776
+ for (const key of keys)
8777
+ if (key in flat) {
8778
+ out[key] = map(flat[key]);
8779
+ delete flat[key];
8780
+ }
8781
+ return out;
8782
+ };
8783
+ var nest = (request, key, flat, defaults = {}) => {
8784
+ const nested = isObject2(decoded(request[key])) ? decoded(request[key]) : null;
8785
+ if (!nested && !Object.keys(flat).length)
8786
+ return;
8787
+ request[key] = { ...defaults, ...flat, ...nested };
8788
+ };
8789
+ var scopeOf = (flat, description) => {
8790
+ const scope = decoded(flat.scope);
8791
+ const has = "paths" in flat || "exclusions" in flat || typeof scope === "string";
8792
+ const out = isObject2(scope) ? scope : has ? {
8793
+ description: typeof scope === "string" ? scope : String(description ?? ""),
8794
+ paths: strings(flat.paths ?? ["."]),
8795
+ exclusions: strings(flat.exclusions ?? [])
8796
+ } : null;
8797
+ delete flat.scope;
8798
+ delete flat.paths;
8799
+ delete flat.exclusions;
8800
+ return out;
8801
+ };
8802
+ var defaultScope = (description) => ({
8803
+ description: typeof description === "string" ? description : "",
8804
+ paths: ["."],
8805
+ exclusions: []
8806
+ });
8807
+ var LISTS = [
8808
+ "decisionIds",
8809
+ "evidenceIds",
8810
+ "findingIds",
8811
+ "requirementIds",
8812
+ "refs",
8813
+ "blockers"
8814
+ ];
8815
+ var ENVELOPE = new Set([
8816
+ "schemaVersion",
8817
+ "action",
8818
+ "taskId",
8819
+ "expectedRevision",
8820
+ "expectedWorkspaceRevision",
8821
+ "judgment"
8822
+ ]);
8823
+ function normalizeOperationInput(family, input, host) {
8824
+ if (!isObject2(input))
8825
+ return input;
8826
+ const request = { ...input };
8827
+ if (!("schemaVersion" in request))
8828
+ request.schemaVersion = 1;
8829
+ else
8830
+ request.schemaVersion = decoded(request.schemaVersion);
8831
+ for (const key of ["limit", "exitCode"])
8832
+ if (key in request)
8833
+ request[key] = decoded(request[key]);
8834
+ const action = request.action;
8835
+ const flat = request;
8836
+ switch (family) {
8837
+ case "task": {
8838
+ if (action === "start" || action === "revise") {
8839
+ const intent = isObject2(decoded(request.intent)) ? decoded(request.intent) : {};
8840
+ const objective = flat.objective ?? intent.objective;
8841
+ const scope = scopeOf(flat, objective);
8842
+ const fields = take(flat, ["objective"]);
8843
+ const authorityRefs = "refs" in flat ? refList(flat.refs) : undefined;
8844
+ delete flat.refs;
8845
+ nest(request, "intent", {
8846
+ ...fields,
8847
+ ...scope ? { scope } : {},
8848
+ ...authorityRefs ? { authorityRefs } : {}
8849
+ }, { scope: defaultScope(objective), authorityRefs: [] });
8850
+ } else if (action === "progress") {
8851
+ const fields = take(flat, ["summary", "nextAction"]);
8852
+ const blockers = "blockers" in flat ? strings(flat.blockers) : undefined;
8853
+ delete flat.blockers;
8854
+ nest(request, "progress", {
8855
+ ...fields,
8856
+ ...Array.isArray(blockers) ? {
8857
+ blockers: blockers.map((item) => typeof item === "string" ? { reason: item, dependentAction: "", refs: [] } : item)
8858
+ } : {}
8859
+ }, { nextAction: null, blockers: [] });
8860
+ } else if (action === "close" && "decisionIds" in flat)
8861
+ flat.decisionIds = strings(flat.decisionIds);
8862
+ break;
8863
+ }
8864
+ case "policy": {
8865
+ if (action === "assess" || action === "preview") {
8866
+ const judgment = isObject2(decoded(request.judgment)) ? { ...decoded(request.judgment) } : {};
8867
+ for (const key of Object.keys(flat))
8868
+ if (!ENVELOPE.has(key)) {
8869
+ if (!(key in judgment))
8870
+ judgment[key] = decoded(flat[key]);
8871
+ delete flat[key];
8872
+ }
8873
+ request.judgment = judgment;
8874
+ }
8875
+ break;
8876
+ }
8877
+ case "evidence": {
8878
+ if (action === "record") {
8879
+ const review = "reviewSession" in flat ? decoded(flat.reviewSession) : undefined;
8880
+ delete flat.reviewSession;
8881
+ const fields = take(flat, [
8882
+ "kind",
8883
+ "claim",
8884
+ "result",
8885
+ "summary",
8886
+ "exitCode",
8887
+ "candidateId",
8888
+ "beforeCandidateId",
8889
+ "reviewContext"
8890
+ ]);
8891
+ if ("refs" in flat)
8892
+ fields.refs = refList(flat.refs);
8893
+ if ("requirementIds" in flat)
8894
+ fields.requirementIds = strings(flat.requirementIds);
8895
+ delete flat.refs;
8896
+ delete flat.requirementIds;
8897
+ if (typeof review === "string" && review)
8898
+ fields.reviewContext = { kind: "host", host, handle: review };
8899
+ nest(request, "evidence", fields, {
8900
+ summary: "",
8901
+ refs: [],
8902
+ requirementIds: [],
8903
+ exitCode: null,
8904
+ reviewContext: null
8905
+ });
8906
+ }
8907
+ break;
8908
+ }
8909
+ case "finding": {
8910
+ if (action === "record") {
8911
+ const scope = scopeOf(flat, flat.claim);
8912
+ if (scope)
8913
+ request.scope = scope;
8914
+ else if (!("scope" in request))
8915
+ request.scope = defaultScope(request.claim);
8916
+ request.refs = "refs" in flat ? refList(flat.refs) : [];
8917
+ }
8918
+ break;
8919
+ }
8920
+ case "decision": {
8921
+ if (action === "record") {
8922
+ const binding = isObject2(decoded(request.binding)) ? decoded(request.binding) : {};
8923
+ const presented = flat.presented ?? binding.presented;
8924
+ const scope = scopeOf(flat, presented);
8925
+ const fields = take(flat, ["presented"]);
8926
+ const choice = flat.choice;
8927
+ delete flat.choice;
8928
+ const contentRefs = "refs" in flat ? refList(flat.refs) : undefined;
8929
+ delete flat.refs;
8930
+ nest(request, "binding", {
8931
+ ...fields,
8932
+ ...scope ? { scope } : {},
8933
+ ...contentRefs ? { contentRefs } : {},
8934
+ ...typeof choice === "string" ? { statedChoice: { ref: "choice", text: choice } } : {}
8935
+ }, { scope: defaultScope(presented), contentRefs: [] });
8936
+ }
8937
+ break;
8938
+ }
8939
+ case "worker": {
8940
+ if (action === "assign") {
8941
+ const objective = flat.objective;
8942
+ const scope = scopeOf(flat, objective);
8943
+ const fields = take(flat, ["role", "objective", "candidateId", "stoppingCondition"]);
8944
+ for (const key of ["decisionIds", "requirementIds"])
8945
+ if (key in flat) {
8946
+ fields[key] = strings(flat[key]);
8947
+ delete flat[key];
8948
+ }
8949
+ nest(request, "assignment", { ...fields, ...scope ? { scope } : {} }, { scope: defaultScope(objective), decisionIds: [], requirementIds: [] });
8950
+ } else if (action === "report") {
8951
+ const fields = take(flat, ["outcome", "summary"]);
8952
+ for (const key of ["evidenceIds", "findingIds"])
8953
+ if (key in flat) {
8954
+ fields[key] = strings(flat[key]);
8955
+ delete flat[key];
8956
+ }
8957
+ nest(request, "report", fields, { evidenceIds: [], findingIds: [] });
8958
+ }
8959
+ break;
8960
+ }
8961
+ case "state": {
8962
+ if ("bundle" in request)
8963
+ request.bundle = decoded(request.bundle);
8964
+ break;
8965
+ }
8966
+ }
8967
+ const defaults = {
8968
+ finding: { resolve: ["evidenceIds", "decisionIds"] },
8969
+ decision: { record: ["requirementIds"] }
8970
+ };
8971
+ for (const key of defaults[family]?.[String(action)] ?? [])
8972
+ if (!(key in request))
8973
+ request[key] = [];
8974
+ for (const key of LISTS)
8975
+ if (key in request && typeof request[key] === "string")
8976
+ request[key] = strings(request[key]);
8977
+ return request;
8978
+ }
8979
+ function parseAdvertisedOperation(family, input, host) {
8980
+ const request = normalizeOperationInput(family, input, host);
8981
+ if (isObject2(request) && request.schemaVersion !== SCHEMA_VERSION)
8982
+ return failure2("unsupported_version", "unsupported schema version", { operation: family });
8983
+ const parsed = advertisedOperationSchemas[family].safeParse(request);
8984
+ if (parsed.success)
8985
+ return success(null, null, request);
8986
+ return failure2("invalid_input", "operation input is invalid", {
8987
+ fields: parsed.error.issues.map((issue) => ({
8988
+ path: issue.code === "unrecognized_keys" ? issue.keys.join(".") : issue.path.join("."),
8989
+ reason: issue.message
8990
+ }))
8991
+ });
8992
+ }
8732
8993
  // packages/workit-core/src/core/task-store.ts
8733
8994
  import { createHash as createHash4, randomUUID as randomUUID3 } from "node:crypto";
8734
8995
  import * as fs7 from "node:fs";
@@ -9783,7 +10044,7 @@ class LogDamage extends Error {
9783
10044
  this.upgrade = upgrade;
9784
10045
  }
9785
10046
  }
9786
- var isObject2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
10047
+ var isObject3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
9787
10048
  var parseEvent = (line, file, offset) => {
9788
10049
  let value;
9789
10050
  try {
@@ -9791,11 +10052,11 @@ var parseEvent = (line, file, offset) => {
9791
10052
  } catch {
9792
10053
  throw new LogDamage(`${file}: unreadable event at byte ${offset}`);
9793
10054
  }
9794
- if (!isObject2(value) || typeof value.v !== "number")
10055
+ if (!isObject3(value) || typeof value.v !== "number")
9795
10056
  throw new LogDamage(`${file}: malformed event at byte ${offset}`);
9796
10057
  if (value.v > EVENT_VERSION)
9797
10058
  throw new LogDamage(`${file}: event format v${value.v} was written by a newer Workit; upgrade Workit to read this task`, true);
9798
- if (typeof value.seq !== "number" || !Number.isInteger(value.seq) || typeof value.type !== "string" || typeof value.id !== "string" || typeof value.task !== "string" || !isObject2(value.data))
10059
+ if (typeof value.seq !== "number" || !Number.isInteger(value.seq) || typeof value.type !== "string" || typeof value.id !== "string" || typeof value.task !== "string" || !isObject3(value.data))
9799
10060
  throw new LogDamage(`${file}: malformed event at byte ${offset}`);
9800
10061
  return value;
9801
10062
  };
@@ -10066,13 +10327,13 @@ var KNOWN_TYPES = new Set([
10066
10327
  "decision.updated",
10067
10328
  "worker.updated"
10068
10329
  ]);
10069
- var isObject3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
10070
- var isKey = (value) => isObject3(value) && typeof value.key === "string" && (value.kind === "branch" || value.kind === "detached" || value.kind === "dir") && (value.branch === null || typeof value.branch === "string");
10330
+ var isObject4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
10331
+ var isKey = (value) => isObject4(value) && typeof value.key === "string" && (value.kind === "branch" || value.kind === "detached" || value.kind === "dir") && (value.branch === null || typeof value.branch === "string");
10071
10332
  var BLOB_REF = "$blob";
10072
- var blobRef = (value) => isObject3(value) && Object.keys(value).length === 1 && typeof value[BLOB_REF] === "string" && /^[0-9a-f]{64}$/.test(value[BLOB_REF]) ? value[BLOB_REF] : null;
10333
+ var blobRef = (value) => isObject4(value) && Object.keys(value).length === 1 && typeof value[BLOB_REF] === "string" && /^[0-9a-f]{64}$/.test(value[BLOB_REF]) ? value[BLOB_REF] : null;
10073
10334
  var blobDigest = (value) => createHash3("sha256").update(JSON.stringify(value)).digest("hex");
10074
10335
  var toBlob = (value, blobs) => {
10075
- if (!isObject3(value))
10336
+ if (!isObject4(value))
10076
10337
  return value;
10077
10338
  const digest = blobDigest(value);
10078
10339
  blobs.write(digest, value);
@@ -10096,7 +10357,7 @@ var decode2 = (value, blobs) => {
10096
10357
  return blobs.read(digest);
10097
10358
  if (Array.isArray(value))
10098
10359
  return value.map((item) => decode2(item, blobs));
10099
- if (isObject3(value)) {
10360
+ if (isObject4(value)) {
10100
10361
  const out = {};
10101
10362
  for (const [key, item] of Object.entries(value))
10102
10363
  out[key] = decode2(item, blobs);
@@ -10115,7 +10376,7 @@ function reduce(state, event, blobs) {
10115
10376
  }
10116
10377
  let next = state;
10117
10378
  if ("record" in data) {
10118
- if (!isObject3(data.record))
10379
+ if (!isObject4(data.record))
10119
10380
  throw new LogDamage(`event ${event.seq} carries a malformed record`);
10120
10381
  next = {
10121
10382
  record: decode2(data.record, blobs),
@@ -10137,7 +10398,7 @@ function reduce(state, event, blobs) {
10137
10398
  }
10138
10399
  if ("key" in data)
10139
10400
  next.key = isKey(data.key) ? data.key : null;
10140
- if ("legacy" in data && isObject3(data.legacy))
10401
+ if ("legacy" in data && isObject4(data.legacy))
10141
10402
  next.legacy = {
10142
10403
  path: String(data.legacy.path ?? ""),
10143
10404
  digest: String(data.legacy.digest ?? "")
@@ -10193,8 +10454,8 @@ var AUTO_COMPACT_BYTES = 2 * 1024 * 1024;
10193
10454
  var TOMBSTONE = Symbol("tombstone");
10194
10455
  var STALE_TEMPORARY_MS = 60 * 60000;
10195
10456
  var INDEX_VERSION = 3;
10196
- var isObject4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
10197
- var hostSession = (value) => isObject4(value) && value.kind === "host" && typeof value.host === "string" && typeof value.handle === "string" ? { host: value.host, handle: value.handle } : null;
10457
+ var isObject5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
10458
+ var hostSession = (value) => isObject5(value) && value.kind === "host" && typeof value.host === "string" && typeof value.handle === "string" ? { host: value.host, handle: value.handle } : null;
10198
10459
  var indexEntry = (task, key, legacy, file) => {
10199
10460
  const sessions = [];
10200
10461
  const intent = hostSession(task.intent.provenance.session);
@@ -11007,7 +11268,7 @@ class TaskStore {
11007
11268
  readSnapshot(taskId) {
11008
11269
  try {
11009
11270
  const value = JSON.parse(fs7.readFileSync(this.snapshotPath(taskId), "utf8"));
11010
- if (!isObject4(value) || value.v !== 1 || value.task !== taskId || typeof value.seq !== "number" || typeof value.lastId !== "string" || typeof value.lastStart !== "number" || typeof value.end !== "number" || !isObject4(value.record))
11271
+ if (!isObject5(value) || value.v !== 1 || value.task !== taskId || typeof value.seq !== "number" || typeof value.lastId !== "string" || typeof value.lastStart !== "number" || typeof value.end !== "number" || !isObject5(value.record))
11011
11272
  return null;
11012
11273
  return value;
11013
11274
  } catch {
@@ -11353,11 +11614,11 @@ class TaskStore {
11353
11614
  readIndex() {
11354
11615
  try {
11355
11616
  const value = JSON.parse(fs7.readFileSync(this.paths().index, "utf8"));
11356
- if (!isObject4(value) || value.version !== INDEX_VERSION || !isObject4(value.tasks))
11617
+ if (!isObject5(value) || value.version !== INDEX_VERSION || !isObject5(value.tasks))
11357
11618
  return {};
11358
11619
  const tasks = {};
11359
11620
  for (const [id, entry] of Object.entries(value.tasks))
11360
- if (isObject4(entry) && entry.id === id && typeof entry.file === "string" && typeof entry.status === "string" && typeof entry.updatedAt === "string" && typeof entry.workspaceId === "string" && Array.isArray(entry.sessions) && isObject4(entry.progress) && isObject4(entry.source))
11621
+ if (isObject5(entry) && entry.id === id && typeof entry.file === "string" && typeof entry.status === "string" && typeof entry.updatedAt === "string" && typeof entry.workspaceId === "string" && Array.isArray(entry.sessions) && isObject5(entry.progress) && isObject5(entry.source))
11361
11622
  tasks[id] = entry;
11362
11623
  return tasks;
11363
11624
  } catch {
@@ -11401,12 +11662,12 @@ class TaskStore {
11401
11662
  return this.parseValue(value, schema, label);
11402
11663
  }
11403
11664
  parseValue(value, schema, label) {
11404
- if (isObject4(value) && "schemaVersion" in value && value.schemaVersion !== SCHEMA_VERSION)
11665
+ if (isObject5(value) && "schemaVersion" in value && value.schemaVersion !== SCHEMA_VERSION)
11405
11666
  return failure2("unsupported_version", `unsupported ${label} schema version`);
11406
11667
  const parsed = parseStoredRecord(schema, value);
11407
11668
  if (parsed.success)
11408
11669
  return success(null, null, parsed.data);
11409
- const writerVersion = isObject4(value) && isObject4(value.runtime) ? value.runtime.updatedWith : null;
11670
+ const writerVersion = isObject5(value) && isObject5(value.runtime) ? value.runtime.updatedWith : null;
11410
11671
  if (parsed.critical.length > 0)
11411
11672
  return failure2("recovery_required", `${label} record requires fields this Workit cannot read (${parsed.critical.join(", ")}); upgrade Workit before mutating this checkout`);
11412
11673
  if (typeof writerVersion === "string" && isNewerVersion(writerVersion, runtimeVersion()))
@@ -11608,7 +11869,7 @@ class TaskStore {
11608
11869
  }
11609
11870
  try {
11610
11871
  const value = JSON.parse(bytes);
11611
- if (isObject4(value) && isObject4(value.store) && value.store.format === "workit-store")
11872
+ if (isObject5(value) && isObject5(value.store) && value.store.format === "workit-store")
11612
11873
  return TOMBSTONE;
11613
11874
  } catch {}
11614
11875
  return { bytes };
@@ -11653,7 +11914,7 @@ class TaskStore {
11653
11914
  isStub(file) {
11654
11915
  try {
11655
11916
  const value = JSON.parse(fs7.readFileSync(file, "utf8"));
11656
- return isObject4(value) && isObject4(value.store) && value.store.format === "workit-store";
11917
+ return isObject5(value) && isObject5(value.store) && value.store.format === "workit-store";
11657
11918
  } catch {
11658
11919
  return false;
11659
11920
  }
@@ -11824,10 +12085,10 @@ var isUncertainWorker = (state) => UNCERTAIN_WORKER_STATES.includes(state);
11824
12085
  var workerBlocksTransition = (transition, state) => transition === "pause" ? false : isUncertainWorker(state);
11825
12086
 
11826
12087
  // packages/workit-core/src/core/task-evaluation.ts
11827
- import { createHash as createHash6 } from "node:crypto";
11828
- import * as fs10 from "node:fs";
11829
- import { spawnSync as spawnSync5 } from "node:child_process";
11830
- import path9 from "node:path";
12088
+ import { createHash as createHash7 } from "node:crypto";
12089
+ import * as fs11 from "node:fs";
12090
+ import { spawnSync as spawnSync6 } from "node:child_process";
12091
+ import path11 from "node:path";
11831
12092
 
11832
12093
  // packages/workit-core/src/check-config.ts
11833
12094
  import { spawnSync as spawnSync3 } from "node:child_process";
@@ -12067,6 +12328,7 @@ var line = (run) => {
12067
12328
  const value = run.stdout.trim();
12068
12329
  return value ? value : null;
12069
12330
  };
12331
+ var safeArg = (value) => value.length > 0 && !value.startsWith("-");
12070
12332
  function headSha(cwd) {
12071
12333
  return line(git2(cwd, ["rev-parse", "--verify", "-q", "HEAD^{commit}"]));
12072
12334
  }
@@ -12158,124 +12420,704 @@ function worktreeSignal(cwd, options = {}) {
12158
12420
  }
12159
12421
  return `sig:${hash.digest("hex")}`;
12160
12422
  }
12161
-
12162
- // packages/workit-core/src/core/task-evaluation.ts
12163
- var digestBytes = (value) => createHash6("sha256").update(value).digest("hex");
12164
- var inside = (root, candidate) => candidate === root || candidate.startsWith(`${root}${path9.sep}`);
12165
- var relative = (root, candidate) => path9.relative(root, candidate).split(path9.sep).join("/") || ".";
12166
- var canonicalPath = (value) => {
12167
- if (typeof value !== "string" || !value)
12168
- return null;
12169
- if (value.includes("\\") || value.split("/").includes(".."))
12170
- return null;
12171
- const normalized = path9.posix.normalize(value);
12172
- if (normalized === ".." || normalized.startsWith("../") || normalized.startsWith("/"))
12423
+ function patchId(cwd, base, head) {
12424
+ if (!safeArg(base) || !safeArg(head))
12173
12425
  return null;
12174
- return normalized === "." ? "." : normalized.replace(/\/$/, "");
12175
- };
12176
- var canonicalScope = (scope) => {
12177
- const paths = scope.paths.length ? scope.paths : ["."];
12178
- const normalizedPaths = paths.map(canonicalPath);
12179
- const normalizedExclusions = scope.exclusions.map(canonicalPath);
12180
- if (normalizedPaths.some((value) => value === null) || normalizedExclusions.some((value) => value === null))
12426
+ const diff = git2(cwd, [
12427
+ "diff",
12428
+ "-U3",
12429
+ "--diff-algorithm=myers",
12430
+ "--indent-heuristic",
12431
+ "--no-color",
12432
+ "--no-ext-diff",
12433
+ "--no-textconv",
12434
+ "--no-renames",
12435
+ "--full-index",
12436
+ "--src-prefix=a/",
12437
+ "--dst-prefix=b/",
12438
+ `${base}...${head}`,
12439
+ "--"
12440
+ ]);
12441
+ if (!diff.ok || !diff.stdout.trim())
12181
12442
  return null;
12443
+ const id = git2(cwd, ["patch-id", "--stable"], { input: diff.stdout });
12444
+ return line(id)?.split(/\s+/u)[0] ?? null;
12445
+ }
12446
+ function currentBranch(cwd) {
12447
+ return line(git2(cwd, ["symbolic-ref", "--short", "-q", "HEAD"]));
12448
+ }
12449
+
12450
+ // packages/workit-core/src/core/policy/derive.ts
12451
+ import { existsSync as existsSync3 } from "node:fs";
12452
+ import path10 from "node:path";
12453
+
12454
+ // packages/workit-core/src/ledger.ts
12455
+ import { spawnSync as spawnSync5 } from "node:child_process";
12456
+ import { createHash as createHash6, randomBytes as randomBytes2 } from "node:crypto";
12457
+ import fs10 from "node:fs";
12458
+ import path9 from "node:path";
12459
+ var LEDGER_VERSION = 1;
12460
+ var PASSING = new Set(["verified", "tests-verified", "type-check-only"]);
12461
+ var FAILING = new Set(["failed", "blocked"]);
12462
+ var STRONG_RESULTS = new Set(["verified", "tests-verified"]);
12463
+ var PR_ROW_TYPES = new Set(["pr.created", "pr.status", "pr.merged"]);
12464
+ var err = (code, error, unblock) => ({
12465
+ ok: false,
12466
+ code,
12467
+ error,
12468
+ ...unblock ? { unblock } : {}
12469
+ });
12470
+ var gitRun2 = (cwd, args, timeoutMs = GIT_TIMEOUTS.local) => {
12471
+ const run = spawnSync5("git", args, {
12472
+ cwd,
12473
+ encoding: "utf8",
12474
+ timeout: timeoutMs,
12475
+ killSignal: "SIGKILL",
12476
+ windowsHide: true,
12477
+ maxBuffer: 256 * 1024 * 1024,
12478
+ env: {
12479
+ ...process.env,
12480
+ GIT_TERMINAL_PROMPT: "0",
12481
+ GIT_OPTIONAL_LOCKS: "0",
12482
+ LC_ALL: "C",
12483
+ LANGUAGE: "C"
12484
+ }
12485
+ });
12182
12486
  return {
12183
- description: scope.description,
12184
- paths: [...new Set(normalizedPaths)],
12185
- exclusions: [...new Set(normalizedExclusions)]
12487
+ ok: run.status === 0,
12488
+ stdout: run.stdout ?? "",
12489
+ stderr: run.error ? String(run.error.message) : run.stderr ?? ""
12186
12490
  };
12187
12491
  };
12188
- var excluded = (value, exclusions) => exclusions.some((item) => item === value || item !== "." && value.startsWith(`${item}/`));
12189
- var scopeMatches = (value, scope) => {
12190
- const normalized = canonicalScope(scope);
12191
- if (!normalized)
12192
- return false;
12193
- const paths = normalized.paths;
12194
- return paths.some((item) => item === "." || value === item || value.startsWith(`${item}/`)) && !excluded(value, normalized.exclusions);
12195
- };
12196
- var pathRelevant = (value, scope) => scopeMatches(value, scope);
12197
- var compareCodeUnits2 = (left, right) => {
12198
- const length = Math.min(left.length, right.length);
12199
- for (let index = 0;index < length; index += 1) {
12200
- const difference = left.charCodeAt(index) - right.charCodeAt(index);
12201
- if (difference !== 0)
12202
- return difference;
12203
- }
12204
- return left.length - right.length;
12492
+ var git3 = (cwd, args) => {
12493
+ const run = gitRun2(cwd, args);
12494
+ const value = run.ok ? run.stdout.trim() : "";
12495
+ return value ? value : null;
12205
12496
  };
12206
- var gitPaths = (root) => {
12207
- const result = spawnSync5("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
12208
- cwd: root,
12209
- encoding: "buffer"
12210
- });
12211
- if (result.status !== 0) {
12212
- const stderr = result.stderr?.toString("utf8") ?? "";
12213
- return {
12214
- paths: [],
12215
- uncertain: !/not a git repository/i.test(stderr),
12216
- git: !/not a git repository/i.test(stderr)
12217
- };
12218
- }
12219
- const paths = result.stdout ? result.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path9.sep).join("/")) : [];
12220
- const stagedDeleted = spawnSync5("git", ["diff", "--cached", "--name-only", "--diff-filter=D", "-z"], { cwd: root, encoding: "buffer" });
12221
- if (stagedDeleted.status !== 0) {
12222
- const stderr = stagedDeleted.stderr?.toString("utf8") ?? "";
12223
- if (!/not a git repository/i.test(stderr))
12224
- return { paths, uncertain: true, git: true };
12225
- } else if (stagedDeleted.stdout) {
12226
- paths.push(...stagedDeleted.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path9.sep).join("/")));
12497
+ var safeRef = (value) => value.length > 0 && !value.startsWith("-");
12498
+ function resolveCommit(cwd, ref) {
12499
+ if (!safeRef(ref))
12500
+ return null;
12501
+ return git3(cwd, ["rev-parse", "--verify", "-q", `${ref}^{commit}`]);
12502
+ }
12503
+ function branchHead(cwd, branch) {
12504
+ if (!safeRef(branch))
12505
+ return null;
12506
+ return resolveCommit(cwd, `refs/heads/${branch}`) ?? resolveCommit(cwd, `refs/remotes/origin/${branch}`);
12507
+ }
12508
+ function defaultBase(cwd) {
12509
+ const originHead = git3(cwd, ["symbolic-ref", "-q", "--short", "refs/remotes/origin/HEAD"]);
12510
+ if (originHead && resolveCommit(cwd, originHead))
12511
+ return originHead;
12512
+ for (const candidate of ["origin/main", "origin/master", "main", "master"])
12513
+ if (resolveCommit(cwd, candidate))
12514
+ return candidate;
12515
+ return null;
12516
+ }
12517
+ function diffHash(cwd, base, head) {
12518
+ if (!safeRef(base) || !safeRef(head))
12519
+ return null;
12520
+ const diff = gitRun2(cwd, [
12521
+ "diff",
12522
+ "-U3",
12523
+ "--diff-algorithm=myers",
12524
+ "--indent-heuristic",
12525
+ "--no-color",
12526
+ "--no-ext-diff",
12527
+ "--no-textconv",
12528
+ "--no-renames",
12529
+ "--full-index",
12530
+ "--binary",
12531
+ "--src-prefix=a/",
12532
+ "--dst-prefix=b/",
12533
+ `${base}...${head}`,
12534
+ "--"
12535
+ ], GIT_TIMEOUTS.worktree);
12536
+ if (!diff.ok || !diff.stdout)
12537
+ return null;
12538
+ const hash = createHash6("sha256");
12539
+ for (const line of diff.stdout.split(`
12540
+ `)) {
12541
+ if (line.startsWith("index "))
12542
+ continue;
12543
+ hash.update(line.startsWith("@@ ") ? "@@" : line);
12544
+ hash.update(`
12545
+ `);
12227
12546
  }
12228
- return { paths: [...new Set(paths)], uncertain: false, git: true };
12547
+ return `sha256:${hash.digest("hex")}`;
12548
+ }
12549
+ function storeRoot(cwd) {
12550
+ const location = resolveStore(path9.resolve(cwd));
12551
+ if (location instanceof Error)
12552
+ return err("unavailable", location.message.replace(/; fix the repository.*$/u, ""), "fix the repository (git status should work here) or run from a non-git directory");
12553
+ return { ok: true, value: { root: location.dir, shared: location.shared } };
12554
+ }
12555
+ function ledgerPath(cwd) {
12556
+ const root = storeRoot(cwd);
12557
+ return root.ok ? { ok: true, value: path9.join(root.value.root, "ledger", "ledger.jsonl") } : root;
12558
+ }
12559
+ var NETWORK_FS_TYPES = new Set([
12560
+ 26985,
12561
+ 20859,
12562
+ 4266872130,
12563
+ 4283649346,
12564
+ 1702057286,
12565
+ 1397113167,
12566
+ 12805120,
12567
+ 16914839,
12568
+ 1196443219,
12569
+ 198183888
12570
+ ]);
12571
+ var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
12572
+ var str2 = (value) => typeof value === "string" ? value : null;
12573
+ var normalizeActor = (value) => {
12574
+ const actor = isRecord2(value) ? value : {};
12575
+ return {
12576
+ host: str2(actor.host) ?? "unknown",
12577
+ session: str2(actor.session),
12578
+ agentId: str2(actor.agentId)
12579
+ };
12229
12580
  };
12230
- var walk = (root, directory, output) => {
12231
- let names;
12581
+ function supersedeAllowed(by, target) {
12582
+ if (by.type !== target.type)
12583
+ return `a ${by.type} row cannot supersede a ${target.type} row`;
12584
+ if (!by.actor.session || by.actor.session !== target.actor.session)
12585
+ return "only the session that wrote a row can supersede it";
12586
+ if (by.type === "verdict" && by.self === true && target.self !== true)
12587
+ return "a self verdict cannot replace an independent verdict";
12588
+ return null;
12589
+ }
12590
+ function readLedger(cwd) {
12591
+ const file = ledgerPath(cwd);
12592
+ if (!file.ok)
12593
+ return file;
12594
+ let text = "";
12232
12595
  try {
12233
- names = fs10.readdirSync(directory);
12234
- } catch {
12235
- return true;
12236
- }
12237
- let uncertain = false;
12238
- for (const name of names) {
12239
- if (name === ".git" || name === ".workit")
12596
+ text = fs10.readFileSync(file.value, "utf8");
12597
+ } catch (error) {
12598
+ if (error.code === "ENOENT")
12599
+ return { ok: true, value: { path: file.value, rows: [], skipped: 0 } };
12600
+ return err("unavailable", `cannot read ${file.value}: ${error.message}`);
12601
+ }
12602
+ const rows = [];
12603
+ let skipped = 0;
12604
+ for (const raw of text.split(`
12605
+ `)) {
12606
+ const trimmed = raw.trim();
12607
+ if (!trimmed)
12240
12608
  continue;
12241
- const target = path9.join(directory, name);
12242
- let stat;
12609
+ let parsed;
12243
12610
  try {
12244
- stat = fs10.lstatSync(target);
12611
+ parsed = JSON.parse(trimmed);
12245
12612
  } catch {
12246
- uncertain = true;
12613
+ skipped += 1;
12247
12614
  continue;
12248
12615
  }
12249
- const item = relative(root, target);
12250
- output.add(item);
12251
- if (stat.isDirectory() && !stat.isSymbolicLink())
12252
- uncertain = walk(root, target, output) || uncertain;
12253
- }
12254
- return uncertain;
12255
- };
12256
- var scopeRoots = (root, scope) => {
12257
- const roots = [];
12258
- for (const item of scope.paths.length ? scope.paths : ["."]) {
12259
- const target = path9.resolve(root, item);
12260
- if (!inside(root, target))
12261
- return failure2("invalid_input", `candidate scope escapes checkout: ${item} — keep one task per repository and coordinate a separate linked task in the other checkout`, { fields: [{ path: "scope.paths", reason: `${item} is outside the checkout` }] });
12262
- let ancestor = target;
12263
- while (!fs10.existsSync(ancestor) && ancestor !== root)
12264
- ancestor = path9.dirname(ancestor);
12265
- try {
12266
- if (!inside(root, fs10.realpathSync(ancestor)))
12267
- return failure2("invalid_input", `candidate scope escapes checkout through a symlink: ${item} — keep one task per repository and coordinate a separate linked task in the other checkout`, { fields: [{ path: "scope.paths", reason: `${item} resolves outside the checkout` }] });
12268
- } catch {
12269
- return failure2("invalid_input", `candidate scope cannot be inspected: ${item}`);
12616
+ if (!isRecord2(parsed) || !str2(parsed.id) || !str2(parsed.type) || !str2(parsed.at)) {
12617
+ skipped += 1;
12618
+ continue;
12270
12619
  }
12271
- roots.push(target);
12620
+ const pr = typeof parsed.pr === "number" && Number.isInteger(parsed.pr) ? parsed.pr : undefined;
12621
+ rows.push({
12622
+ ...parsed,
12623
+ v: typeof parsed.v === "number" ? parsed.v : LEDGER_VERSION,
12624
+ id: parsed.id,
12625
+ at: parsed.at,
12626
+ type: parsed.type,
12627
+ actor: normalizeActor(parsed.actor),
12628
+ branch: str2(parsed.branch),
12629
+ head: str2(parsed.head),
12630
+ base: str2(parsed.base),
12631
+ baseSha: str2(parsed.baseSha),
12632
+ patchId: str2(parsed.patchId),
12633
+ diffHash: str2(parsed.diffHash),
12634
+ tree: str2(parsed.tree),
12635
+ dirty: typeof parsed.dirty === "boolean" ? parsed.dirty : null,
12636
+ ...pr === undefined ? {} : { pr },
12637
+ ...str2(parsed.supersedes) ? { supersedes: parsed.supersedes } : {},
12638
+ seq: rows.length + 1,
12639
+ superseded: false,
12640
+ supersedeIgnored: false
12641
+ });
12272
12642
  }
12273
- return success(null, null, roots);
12274
- };
12275
- function captureCandidate(root, scope, environment = []) {
12276
- let checkout;
12277
- try {
12278
- checkout = fs10.realpathSync(root);
12643
+ const byId = new Map;
12644
+ for (const row of rows) {
12645
+ const target = row.supersedes ? byId.get(row.supersedes) : undefined;
12646
+ if (target && supersedeAllowed(row, target) === null)
12647
+ target.superseded = true;
12648
+ else if (row.supersedes)
12649
+ row.supersedeIgnored = true;
12650
+ if (!byId.has(row.id))
12651
+ byId.set(row.id, row);
12652
+ }
12653
+ return { ok: true, value: { path: file.value, rows, skipped } };
12654
+ }
12655
+ function verdictBasis(verdict, current) {
12656
+ if (verdict.head && verdict.head === current.head)
12657
+ return "fresh";
12658
+ if (verdict.patchId && verdict.patchId === current.patchId && verdict.diffHash && verdict.diffHash === current.diffHash)
12659
+ return "carried";
12660
+ return "stale";
12661
+ }
12662
+ var isLiveVerdict = (row) => row.type === "verdict" && typeof row.result === "string" && !row.superseded;
12663
+ function trailerSessions(cwd, base, head) {
12664
+ if (!base || !head || !safeRef(base) || !safeRef(head))
12665
+ return [];
12666
+ const out = git3(cwd, [
12667
+ "log",
12668
+ "--format=%(trailers:key=Workit-Session,valueonly,separator=%x00)",
12669
+ `${base}..${head}`
12670
+ ]);
12671
+ return (out ?? "").split(/[\0\n]/u).map((value) => value.trim()).filter(Boolean);
12672
+ }
12673
+ function authorSessions(cwd, rows, branch, range = { base: null, head: null }) {
12674
+ const sessions = new Set;
12675
+ for (const row of rows) {
12676
+ if (row.branch !== branch || row.type !== "commit.recorded" && row.type !== "task.opened")
12677
+ continue;
12678
+ const session = str2(row.session) ?? row.actor.session;
12679
+ if (session)
12680
+ sessions.add(session);
12681
+ }
12682
+ for (const session of trailerSessions(cwd, range.base, range.head))
12683
+ sessions.add(session);
12684
+ return sessions;
12685
+ }
12686
+ var independenceReasons = (row, authors) => {
12687
+ const reasons = [];
12688
+ if (!row.actor.session)
12689
+ reasons.push("no_session");
12690
+ else if (authors.has(row.actor.session))
12691
+ reasons.push("author_session");
12692
+ if (row.self === true && !reasons.includes("no_session"))
12693
+ reasons.push("self");
12694
+ return reasons;
12695
+ };
12696
+ function effectiveOf(rows, authors) {
12697
+ let effective = null;
12698
+ for (const row of rows) {
12699
+ if (!effective) {
12700
+ effective = row;
12701
+ continue;
12702
+ }
12703
+ const rowIndependent = independenceReasons(row, authors).length === 0;
12704
+ const effIndependent = independenceReasons(effective, authors).length === 0;
12705
+ if (effIndependent && !rowIndependent)
12706
+ continue;
12707
+ if (effIndependent && FAILING.has(String(effective.result)) && row.actor.session === effective.actor.session)
12708
+ continue;
12709
+ effective = row;
12710
+ }
12711
+ return effective;
12712
+ }
12713
+ function restackCarries(rows, branch, from, to) {
12714
+ if (!from || !to)
12715
+ return false;
12716
+ const edges = new Map;
12717
+ for (const row of rows)
12718
+ if (row.type === "stack.restacked" && row.observer === "workit_cli" && row.branch === branch && row.patchEqual === true && typeof row.fromHead === "string" && row.head)
12719
+ edges.set(row.fromHead, [...edges.get(row.fromHead) ?? [], row.head]);
12720
+ const seen = new Set([from]);
12721
+ const queue = [from];
12722
+ while (queue.length) {
12723
+ const next = queue.shift();
12724
+ if (next === to)
12725
+ return true;
12726
+ for (const target of edges.get(next) ?? [])
12727
+ if (!seen.has(target)) {
12728
+ seen.add(target);
12729
+ queue.push(target);
12730
+ }
12731
+ }
12732
+ return false;
12733
+ }
12734
+ function checkVerdicts(cwd, branch, rows) {
12735
+ const head = branchHead(cwd, branch);
12736
+ const fallbackBase = defaultBase(cwd);
12737
+ const keyCache = new Map;
12738
+ const keyFor = (base) => {
12739
+ if (!base || !head)
12740
+ return { patchId: null, diffHash: null };
12741
+ let key = keyCache.get(base);
12742
+ if (!key) {
12743
+ key = { patchId: patchId(cwd, base, head), diffHash: diffHash(cwd, base, head) };
12744
+ keyCache.set(base, key);
12745
+ }
12746
+ return key;
12747
+ };
12748
+ const authors = authorSessions(cwd, rows, branch, { base: fallbackBase, head });
12749
+ const byKind = new Map;
12750
+ for (const row of rows)
12751
+ if (row.branch === branch && isLiveVerdict(row)) {
12752
+ const kind = typeof row.kind === "string" ? row.kind : "review";
12753
+ byKind.set(kind, [...byKind.get(kind) ?? [], row]);
12754
+ }
12755
+ const verdicts = [];
12756
+ for (const [kind, list] of byKind) {
12757
+ const row = effectiveOf(list, authors);
12758
+ if (!row)
12759
+ continue;
12760
+ let basis = verdictBasis(row, { head, ...keyFor(row.base ?? fallbackBase) });
12761
+ if (basis === "stale" && restackCarries(rows, branch, row.head, head))
12762
+ basis = "carried";
12763
+ const current = basis !== "stale";
12764
+ const independence = independenceReasons(row, authors);
12765
+ const reasons = [
12766
+ ...current ? [] : ["stale"],
12767
+ ...PASSING.has(String(row.result)) ? [] : ["not_passing"],
12768
+ ...independence
12769
+ ];
12770
+ verdicts.push({
12771
+ kind,
12772
+ basis,
12773
+ current,
12774
+ independent: independence.length === 0,
12775
+ accepted: reasons.length === 0,
12776
+ reasons,
12777
+ verdict: row
12778
+ });
12779
+ }
12780
+ verdicts.sort((a, b) => a.verdict.seq - b.verdict.seq);
12781
+ const newestCurrent = verdicts.findLast((entry) => entry.current) ?? verdicts.at(-1) ?? null;
12782
+ const failing = verdicts.find((entry) => entry.current && entry.independent && FAILING.has(String(entry.verdict.result)));
12783
+ const newestAccepted = verdicts.findLast((entry) => entry.accepted) ?? null;
12784
+ const accepted = newestAccepted && !failing ? { accepted: true, verdict: newestAccepted.verdict, reasons: [] } : {
12785
+ accepted: false,
12786
+ verdict: failing?.verdict ?? null,
12787
+ reasons: failing ? ["failing_verdict"] : newestCurrent?.reasons ?? ["no_verdict"]
12788
+ };
12789
+ const selfEntry = failing ? undefined : verdicts.findLast((entry) => entry.current && !entry.independent && STRONG_RESULTS.has(String(entry.verdict.result)));
12790
+ return {
12791
+ branch,
12792
+ head,
12793
+ review: accepted.accepted ? "verified" : selfEntry ? "self-reviewed" : "unreviewed",
12794
+ selfVerdict: accepted.accepted ? null : selfEntry?.verdict ?? null,
12795
+ base: newestCurrent?.verdict.base ?? fallbackBase,
12796
+ patchId: keyFor(newestCurrent?.verdict.base ?? fallbackBase).patchId,
12797
+ current: { basis: newestCurrent?.basis ?? "none", verdict: newestCurrent?.verdict ?? null },
12798
+ accepted,
12799
+ verdicts,
12800
+ authors: [...authors].toSorted()
12801
+ };
12802
+ }
12803
+ var OBSERVED_ONLY = new Set(["check", ...PR_ROW_TYPES]);
12804
+
12805
+ // packages/workit-core/src/core/policy/derive.ts
12806
+ var RULES = {
12807
+ test: "check:test",
12808
+ self: "verdict:self",
12809
+ nonAuthor: "verdict:non-author",
12810
+ verified: "verdict:verified",
12811
+ product: "decision:product",
12812
+ plan: "plan"
12813
+ };
12814
+ var derived2 = (spec) => ({
12815
+ ...spec,
12816
+ id: requirementId({
12817
+ ruleId: spec.ruleId,
12818
+ scope: spec.scope,
12819
+ satisfaction: spec.satisfaction,
12820
+ before: spec.before,
12821
+ dependentAction: spec.dependentAction
12822
+ })
12823
+ });
12824
+ var sorted = (values) => [...new Set(values)].toSorted();
12825
+ var normalizedScope = (scope) => ({
12826
+ description: scope.description,
12827
+ paths: sorted(scope.paths),
12828
+ exclusions: sorted(scope.exclusions)
12829
+ });
12830
+ function deriveRequirements(judgment, scope, constraints, verification = "self") {
12831
+ const out = [];
12832
+ const base = { scope, dependentAction: null };
12833
+ if (judgment.behaviorChange)
12834
+ out.push(derived2({
12835
+ ...base,
12836
+ ruleId: RULES.test,
12837
+ dimension: "testing",
12838
+ reason: "Behavior changes: the configured test check must pass on the final tree.",
12839
+ satisfaction: "Run `workit check test` on the final tree.",
12840
+ before: "close",
12841
+ acceptanceAllowed: false
12842
+ }));
12843
+ if (judgment.behaviorChange && judgment.riskTier === "normal")
12844
+ out.push(verification === "independent" ? derived2({
12845
+ ...base,
12846
+ ruleId: RULES.nonAuthor,
12847
+ dimension: "review",
12848
+ reason: "This workspace requires independent verification: a session that did not author the change records the verdict.",
12849
+ satisfaction: "An independent session records a passing verdict: `workit ledger verdict verified --how <what it exercised>`.",
12850
+ before: "close",
12851
+ acceptanceAllowed: false
12852
+ }) : derived2({
12853
+ ...base,
12854
+ ruleId: RULES.self,
12855
+ dimension: "review",
12856
+ reason: "A behavior change needs a recorded verdict; the author's own is allowed and is shown as self-reviewed, never verified.",
12857
+ satisfaction: "After an observed passing `workit check test`, record `workit ledger verdict tests-verified --self --how <what you ran>` (or have a verifier record one).",
12858
+ before: "close",
12859
+ acceptanceAllowed: false
12860
+ }));
12861
+ if (judgment.riskTier === "high")
12862
+ out.push(derived2({
12863
+ ...base,
12864
+ ruleId: RULES.verified,
12865
+ dimension: "review",
12866
+ reason: "High risk: an independent verifier must exercise the live surface.",
12867
+ satisfaction: "An independent verifier runs the real surface and records `workit ledger verdict verified --kind live --how <what it exercised>`.",
12868
+ before: "close",
12869
+ acceptanceAllowed: false
12870
+ }));
12871
+ if (judgment.productChoiceOpen)
12872
+ out.push(derived2({
12873
+ ...base,
12874
+ ruleId: RULES.product,
12875
+ dimension: "decisions",
12876
+ reason: "A product or preference choice is open; the user decides it before code is written.",
12877
+ satisfaction: 'Ask the user (with your recommendation), then record the answer: `workit ledger decision "<choice>" --why "<reason>"`.',
12878
+ before: "write",
12879
+ acceptanceAllowed: false
12880
+ }));
12881
+ if (judgment.needsPlan || judgment.riskTier === "high")
12882
+ out.push(derived2({
12883
+ ...base,
12884
+ ruleId: RULES.plan,
12885
+ dimension: "artifacts",
12886
+ reason: judgment.needsPlan ? "The work needs a written plan or spec before implementation." : "High-risk work needs a written plan before implementation.",
12887
+ satisfaction: "Write the plan or spec (e.g. docs/plans/<slug>.md; docs and Markdown writes stay allowed), then record it: `workit policy assess --ref <path>`.",
12888
+ before: "write",
12889
+ acceptanceAllowed: true
12890
+ }));
12891
+ for (const constraint of constraints)
12892
+ for (const obligation of constraint.requires)
12893
+ out.push(derived2({
12894
+ ruleId: `project-constraint:${constraint.id}:${obligation.kind}:${sha256({
12895
+ kind: obligation.kind,
12896
+ scope: normalizedScope(obligation.scope),
12897
+ refs: obligation.refs.map((ref) => JSON.stringify(ref)).toSorted(),
12898
+ method: obligation.method,
12899
+ before: obligation.before,
12900
+ dependentAction: obligation.dependentAction
12901
+ })}`,
12902
+ dimension: obligation.method ?? (obligation.kind === "check" ? "verification" : obligation.kind === "decision" ? "decisions" : "challenge"),
12903
+ scope: obligation.scope,
12904
+ reason: `${constraint.kind} constraint requires ${obligation.kind}: ${constraint.statement}`,
12905
+ satisfaction: `Satisfy the constraint obligation using its referenced ${obligation.kind}.`,
12906
+ before: obligation.before,
12907
+ dependentAction: obligation.dependentAction,
12908
+ acceptanceAllowed: constraint.acceptanceAllowed && obligation.kind !== "decision"
12909
+ }));
12910
+ const seen = new Set;
12911
+ return out.filter((item) => seen.has(item.id) ? false : (seen.add(item.id), true));
12912
+ }
12913
+ var latestJudgment = (task) => task.judgments?.at(-1)?.data ?? null;
12914
+ var ledgerView = (root) => {
12915
+ let branch;
12916
+ let rows;
12917
+ return {
12918
+ branch: () => branch === undefined ? branch = currentBranch(root) : branch,
12919
+ rows: () => {
12920
+ if (rows === undefined) {
12921
+ const read = readLedger(root);
12922
+ rows = read.ok ? read.value.rows : [];
12923
+ }
12924
+ return rows;
12925
+ }
12926
+ };
12927
+ };
12928
+ var productDecision = (task, ledger) => {
12929
+ const decisions = task.decisions.filter((entry) => entry.data.revoked === null && (entry.data.response === "approved" || entry.data.response === "stated") && entry.data.purpose !== "limitation");
12930
+ if (decisions.length)
12931
+ return {
12932
+ met: true,
12933
+ reason: "the user's decision is recorded on the task",
12934
+ decisionIds: decisions.map((entry) => entry.id)
12935
+ };
12936
+ const since = Date.parse(task.createdAt);
12937
+ const branch = ledger.branch();
12938
+ const row = ledger.rows().findLast((item) => item.type === "decision" && !item.superseded && (item.branch ?? null) === branch && Date.parse(item.at) >= since);
12939
+ return row ? { met: true, reason: `ledger decision ${row.id} records the choice`, decisionIds: [] } : {
12940
+ met: false,
12941
+ reason: 'open product choice: ask the user, then `workit ledger decision "<choice>" --why "<reason>"` (or re-judge productChoiceOpen=false if it is not open)',
12942
+ decisionIds: []
12943
+ };
12944
+ };
12945
+ var planRecorded = (task, root) => {
12946
+ const cited = (latestJudgment(task)?.refs ?? []).find((ref) => ref.kind === "external" || ref.kind === "file" && existsSync3(path10.resolve(root, ref.path)));
12947
+ if (cited)
12948
+ return {
12949
+ met: true,
12950
+ reason: `plan recorded: ${cited.kind === "file" ? cited.path : cited.kind === "external" ? cited.url : ""}`,
12951
+ decisionIds: []
12952
+ };
12953
+ const artifact = task.evidence.some((entry) => entry.data.kind === "artifact" && entry.data.result === "passed");
12954
+ return artifact ? { met: true, reason: "a passing artifact records the plan", decisionIds: [] } : {
12955
+ met: false,
12956
+ reason: "no plan recorded: write it (docs/ and Markdown writes stay allowed), then `workit policy assess --ref <path>`",
12957
+ decisionIds: []
12958
+ };
12959
+ };
12960
+ var verdict = (requirement, root, ledger) => {
12961
+ const branch = ledger.branch();
12962
+ if (!branch)
12963
+ return { met: false, reason: "no branch is checked out to hold a verdict", decisionIds: [] };
12964
+ const check = checkVerdicts(root, branch, ledger.rows());
12965
+ const failing = check.accepted.reasons.includes("failing_verdict");
12966
+ const strong = (entry) => STRONG_RESULTS.has(String(entry.verdict.result));
12967
+ const met = !failing && (requirement.ruleId === RULES.verified ? check.verdicts.find((entry) => entry.accepted && entry.kind === "live" && String(entry.verdict.result) === "verified") : requirement.ruleId === RULES.nonAuthor ? check.verdicts.find((entry) => entry.accepted && strong(entry)) : check.verdicts.find((entry) => entry.current && strong(entry)));
12968
+ if (met)
12969
+ return {
12970
+ met: true,
12971
+ reason: `${met.accepted ? "accepted independent" : "self-reviewed (author's own)"} ${String(met.verdict.result)} verdict ${met.verdict.id}`,
12972
+ decisionIds: []
12973
+ };
12974
+ const why = failing ? "a current independent verdict fails" : check.verdicts.some((entry) => entry.current && entry.verdict.result === "type-check-only") ? "type-check-only does not prove a behavior change" : (check.accepted.reasons.join(", ") || "no_verdict").replace(/_/g, " ");
12975
+ const unblock = requirement.ruleId === RULES.self ? "record your own verdict after an observed `workit check test`: `workit ledger verdict tests-verified --self --how <what you ran>` (shown as self-reviewed), or have a verifier record one" : `an independent session records \`workit ledger verdict verified${requirement.ruleId === RULES.verified ? " --kind live" : ""} --how <what it exercised>\``;
12976
+ return { met: false, reason: `${why}: ${unblock}`, decisionIds: [] };
12977
+ };
12978
+ function derivedRuleStatus(task, requirement, root, ledger) {
12979
+ switch (requirement.ruleId) {
12980
+ case RULES.product:
12981
+ return productDecision(task, ledger);
12982
+ case RULES.plan:
12983
+ return planRecorded(task, root);
12984
+ case RULES.self:
12985
+ case RULES.nonAuthor:
12986
+ case RULES.verified:
12987
+ return verdict(requirement, root, ledger);
12988
+ default:
12989
+ return null;
12990
+ }
12991
+ }
12992
+ function writeBlockers(task, root) {
12993
+ if (task.status === "closed" || !task.policy)
12994
+ return [];
12995
+ const ledger = ledgerView(root);
12996
+ return task.policy.requirements.flatMap((requirement) => {
12997
+ if (requirement.before !== "write")
12998
+ return [];
12999
+ const status = derivedRuleStatus(task, requirement, root, ledger);
13000
+ return status && !status.met ? [{ ruleId: requirement.ruleId, reason: status.reason }] : [];
13001
+ });
13002
+ }
13003
+
13004
+ // packages/workit-core/src/core/task-evaluation.ts
13005
+ var digestBytes = (value) => createHash7("sha256").update(value).digest("hex");
13006
+ var inside = (root, candidate) => candidate === root || candidate.startsWith(`${root}${path11.sep}`);
13007
+ var relative = (root, candidate) => path11.relative(root, candidate).split(path11.sep).join("/") || ".";
13008
+ var canonicalPath = (value) => {
13009
+ if (typeof value !== "string" || !value)
13010
+ return null;
13011
+ if (value.includes("\\") || value.split("/").includes(".."))
13012
+ return null;
13013
+ const normalized = path11.posix.normalize(value);
13014
+ if (normalized === ".." || normalized.startsWith("../") || normalized.startsWith("/"))
13015
+ return null;
13016
+ return normalized === "." ? "." : normalized.replace(/\/$/, "");
13017
+ };
13018
+ var canonicalScope = (scope) => {
13019
+ const paths = scope.paths.length ? scope.paths : ["."];
13020
+ const normalizedPaths = paths.map(canonicalPath);
13021
+ const normalizedExclusions = scope.exclusions.map(canonicalPath);
13022
+ if (normalizedPaths.some((value) => value === null) || normalizedExclusions.some((value) => value === null))
13023
+ return null;
13024
+ return {
13025
+ description: scope.description,
13026
+ paths: [...new Set(normalizedPaths)],
13027
+ exclusions: [...new Set(normalizedExclusions)]
13028
+ };
13029
+ };
13030
+ var excluded = (value, exclusions) => exclusions.some((item) => item === value || item !== "." && value.startsWith(`${item}/`));
13031
+ var scopeMatches = (value, scope) => {
13032
+ const normalized = canonicalScope(scope);
13033
+ if (!normalized)
13034
+ return false;
13035
+ const paths = normalized.paths;
13036
+ return paths.some((item) => item === "." || value === item || value.startsWith(`${item}/`)) && !excluded(value, normalized.exclusions);
13037
+ };
13038
+ var pathRelevant = (value, scope) => scopeMatches(value, scope);
13039
+ var compareCodeUnits2 = (left, right) => {
13040
+ const length = Math.min(left.length, right.length);
13041
+ for (let index = 0;index < length; index += 1) {
13042
+ const difference = left.charCodeAt(index) - right.charCodeAt(index);
13043
+ if (difference !== 0)
13044
+ return difference;
13045
+ }
13046
+ return left.length - right.length;
13047
+ };
13048
+ var gitPaths = (root) => {
13049
+ const result = spawnSync6("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
13050
+ cwd: root,
13051
+ encoding: "buffer"
13052
+ });
13053
+ if (result.status !== 0) {
13054
+ const stderr = result.stderr?.toString("utf8") ?? "";
13055
+ return {
13056
+ paths: [],
13057
+ uncertain: !/not a git repository/i.test(stderr),
13058
+ git: !/not a git repository/i.test(stderr)
13059
+ };
13060
+ }
13061
+ const paths = result.stdout ? result.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path11.sep).join("/")) : [];
13062
+ const stagedDeleted = spawnSync6("git", ["diff", "--cached", "--name-only", "--diff-filter=D", "-z"], { cwd: root, encoding: "buffer" });
13063
+ if (stagedDeleted.status !== 0) {
13064
+ const stderr = stagedDeleted.stderr?.toString("utf8") ?? "";
13065
+ if (!/not a git repository/i.test(stderr))
13066
+ return { paths, uncertain: true, git: true };
13067
+ } else if (stagedDeleted.stdout) {
13068
+ paths.push(...stagedDeleted.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path11.sep).join("/")));
13069
+ }
13070
+ return { paths: [...new Set(paths)], uncertain: false, git: true };
13071
+ };
13072
+ var walk = (root, directory, output) => {
13073
+ let names;
13074
+ try {
13075
+ names = fs11.readdirSync(directory);
13076
+ } catch {
13077
+ return true;
13078
+ }
13079
+ let uncertain = false;
13080
+ for (const name of names) {
13081
+ if (name === ".git" || name === ".workit")
13082
+ continue;
13083
+ const target = path11.join(directory, name);
13084
+ let stat;
13085
+ try {
13086
+ stat = fs11.lstatSync(target);
13087
+ } catch {
13088
+ uncertain = true;
13089
+ continue;
13090
+ }
13091
+ const item = relative(root, target);
13092
+ output.add(item);
13093
+ if (stat.isDirectory() && !stat.isSymbolicLink())
13094
+ uncertain = walk(root, target, output) || uncertain;
13095
+ }
13096
+ return uncertain;
13097
+ };
13098
+ var scopeRoots = (root, scope) => {
13099
+ const roots = [];
13100
+ for (const item of scope.paths.length ? scope.paths : ["."]) {
13101
+ const target = path11.resolve(root, item);
13102
+ if (!inside(root, target))
13103
+ return failure2("invalid_input", `candidate scope escapes checkout: ${item} — keep one task per repository and coordinate a separate linked task in the other checkout`, { fields: [{ path: "scope.paths", reason: `${item} is outside the checkout` }] });
13104
+ let ancestor = target;
13105
+ while (!fs11.existsSync(ancestor) && ancestor !== root)
13106
+ ancestor = path11.dirname(ancestor);
13107
+ try {
13108
+ if (!inside(root, fs11.realpathSync(ancestor)))
13109
+ return failure2("invalid_input", `candidate scope escapes checkout through a symlink: ${item} — keep one task per repository and coordinate a separate linked task in the other checkout`, { fields: [{ path: "scope.paths", reason: `${item} resolves outside the checkout` }] });
13110
+ } catch {
13111
+ return failure2("invalid_input", `candidate scope cannot be inspected: ${item}`);
13112
+ }
13113
+ roots.push(target);
13114
+ }
13115
+ return success(null, null, roots);
13116
+ };
13117
+ function captureCandidate(root, scope, environment = []) {
13118
+ let checkout;
13119
+ try {
13120
+ checkout = fs11.realpathSync(root);
12279
13121
  } catch {
12280
13122
  return failure2("invalid_input", "candidate root does not exist");
12281
13123
  }
@@ -12293,11 +13135,11 @@ function captureCandidate(root, scope, environment = []) {
12293
13135
  if (item === ".git" || item === ".workit")
12294
13136
  continue;
12295
13137
  names.add(item);
12296
- if (!fs10.existsSync(target))
13138
+ if (!fs11.existsSync(target))
12297
13139
  continue;
12298
13140
  let stat;
12299
13141
  try {
12300
- stat = fs10.lstatSync(target);
13142
+ stat = fs11.lstatSync(target);
12301
13143
  } catch {
12302
13144
  uncertain = true;
12303
13145
  continue;
@@ -12313,17 +13155,17 @@ function captureCandidate(root, scope, environment = []) {
12313
13155
  for (const item of [...names].toSorted()) {
12314
13156
  if (!scopeMatches(item, normalizedScope))
12315
13157
  continue;
12316
- const target = path9.join(checkout, item);
13158
+ const target = path11.join(checkout, item);
12317
13159
  try {
12318
- const stat = fs10.lstatSync(target);
13160
+ const stat = fs11.lstatSync(target);
12319
13161
  if (stat.isSymbolicLink()) {
12320
- const link = fs10.readlinkSync(target);
13162
+ const link = fs11.readlinkSync(target);
12321
13163
  files.push({ path: item, kind: "symlink", digest: digestBytes(link), executable: null });
12322
13164
  } else if (stat.isFile()) {
12323
13165
  files.push({
12324
13166
  path: item,
12325
13167
  kind: "file",
12326
- digest: digestBytes(fs10.readFileSync(target)),
13168
+ digest: digestBytes(fs11.readFileSync(target)),
12327
13169
  executable: (stat.mode & 73) !== 0
12328
13170
  });
12329
13171
  } else if (!stat.isDirectory()) {
@@ -12348,7 +13190,7 @@ function captureCandidate(root, scope, environment = []) {
12348
13190
  const values = supplied.toSorted((left, right) => compareCodeUnits2(left.name, right.name)).map(({ name, value }) => ({ name, value, refs: [] }));
12349
13191
  if (values.some(({ value }) => value === null))
12350
13192
  completeness = "uncertain";
12351
- const headResult = spawnSync5("git", ["rev-parse", "HEAD"], { cwd: checkout, encoding: "utf8" });
13193
+ const headResult = spawnSync6("git", ["rev-parse", "HEAD"], { cwd: checkout, encoding: "utf8" });
12352
13194
  if (headResult.status !== 0 && !/not a git repository/i.test(headResult.stderr ?? ""))
12353
13195
  completeness = "uncertain";
12354
13196
  const head = headResult.status === 0 ? headResult.stdout.trim() : null;
@@ -12488,7 +13330,7 @@ var findingVerificationPasses = (findingCandidateId, evidence) => evidence.statu
12488
13330
  var verifyDecisionContentAtRoot = (checkoutRoot, binding) => {
12489
13331
  let root;
12490
13332
  try {
12491
- root = fs10.realpathSync(checkoutRoot);
13333
+ root = fs11.realpathSync(checkoutRoot);
12492
13334
  } catch {
12493
13335
  return failure2("permission_denied", "checkout root is unavailable");
12494
13336
  }
@@ -12497,23 +13339,23 @@ var verifyDecisionContentAtRoot = (checkoutRoot, binding) => {
12497
13339
  continue;
12498
13340
  if (!reference.digest)
12499
13341
  return failure2("invalid_input", "document references require a byte digest");
12500
- const target = path9.resolve(root, reference.path);
12501
- if (target !== root && !target.startsWith(`${root}${path9.sep}`))
13342
+ const target = path11.resolve(root, reference.path);
13343
+ if (target !== root && !target.startsWith(`${root}${path11.sep}`))
12502
13344
  return failure2("invalid_input", "document reference escapes checkout; cite it as an external file:// reference instead", {
12503
13345
  fields: [
12504
13346
  { path: "contentRefs.path", reason: `${reference.path} is outside the checkout` }
12505
13347
  ]
12506
13348
  });
12507
13349
  try {
12508
- const below = path9.relative(root, target);
13350
+ const below = path11.relative(root, target);
12509
13351
  let current = root;
12510
- for (const segment of below.split(path9.sep)) {
13352
+ for (const segment of below.split(path11.sep)) {
12511
13353
  if (!segment)
12512
13354
  continue;
12513
- current = path9.join(current, segment);
12514
- const stat = fs10.lstatSync(current);
12515
- const real = fs10.realpathSync(current);
12516
- if (real !== root && !real.startsWith(`${root}${path9.sep}`))
13355
+ current = path11.join(current, segment);
13356
+ const stat = fs11.lstatSync(current);
13357
+ const real = fs11.realpathSync(current);
13358
+ if (real !== root && !real.startsWith(`${root}${path11.sep}`))
12517
13359
  return failure2("invalid_input", "document reference escapes checkout; cite it as an external file:// reference instead", {
12518
13360
  fields: [
12519
13361
  {
@@ -12529,7 +13371,7 @@ var verifyDecisionContentAtRoot = (checkoutRoot, binding) => {
12529
13371
  if (current !== target && !stat.isDirectory())
12530
13372
  return failure2("invalid_input", "document reference ancestor is not a directory");
12531
13373
  }
12532
- if (digestBytes(fs10.readFileSync(target)) !== reference.digest)
13374
+ if (digestBytes(fs11.readFileSync(target)) !== reference.digest)
12533
13375
  return failure2("permission_denied", "decision document bytes have changed");
12534
13376
  } catch {
12535
13377
  return failure2("invalid_input", "decision document is unavailable");
@@ -12614,7 +13456,35 @@ function evaluateRequirements(task, workspace, capabilities, candidate, checkout
12614
13456
  decisionIds: [],
12615
13457
  reason: "stored policy version is unsupported; reassessment is required"
12616
13458
  }));
13459
+ const ledger = ledgerView(checkoutRoot);
12617
13460
  return (task.policy?.requirements ?? []).map((requirement) => {
13461
+ const derived = derivedRuleStatus(task, requirement, checkoutRoot, ledger);
13462
+ if (derived) {
13463
+ if (derived.met)
13464
+ return {
13465
+ requirementId: requirement.id,
13466
+ status: "satisfied",
13467
+ evidenceIds: [],
13468
+ decisionIds: derived.decisionIds,
13469
+ reason: derived.reason
13470
+ };
13471
+ const waived = requirement.acceptanceAllowed ? applicableDecision(task, workspace, requirement, checkoutRoot) : [];
13472
+ if (waived.length)
13473
+ return {
13474
+ requirementId: requirement.id,
13475
+ status: "accepted_limitation",
13476
+ evidenceIds: [],
13477
+ decisionIds: waived.map((decision) => task.decisions.find((entry) => entry.data === decision).id),
13478
+ reason: "an applicable approved limitation permits the missing evidence"
13479
+ };
13480
+ return {
13481
+ requirementId: requirement.id,
13482
+ status: "unsatisfied",
13483
+ evidenceIds: [],
13484
+ decisionIds: [],
13485
+ reason: derived.reason
13486
+ };
13487
+ }
12618
13488
  const gate = observedGate(requirement) ? evaluateObservedGate(task, requirement, statuses, configFor()) : null;
12619
13489
  if (gate?.satisfied.length)
12620
13490
  return {
@@ -12869,10 +13739,10 @@ Autonomy contract:
12869
13739
  inferred, or guess. Prose is a note: only a configured check the CLI ran
12870
13740
  (\`workit check <name>\`) satisfies a gate; an ad-hoc
12871
13741
  \`workit check -- <cmd>\` never does.
12872
- - Author is not verifier: never record a passing verdict on work your session
12873
- wrote. The lead starts each verifier with its own session
12874
- (\`WORKIT_SESSION_ID=<lead>-v<n>\`; Claude Code subagents get one from the
12875
- hook), and the verifier records \`workit ledger verdict\`.
13742
+ - Author is not verifier: your own verdict is \`--self\` (after an observed
13743
+ \`workit check test\`), shown as self-reviewed, never verified. High risk,
13744
+ \`verification: independent\` and merge need a verifier with its own
13745
+ session (\`WORKIT_SESSION_ID=<lead>-v<n>\`; Claude Code subagents get one).
12876
13746
  - A local-commit endpoint does not imply PR readiness, and a local commit alone
12877
13747
  is not evidence of a requested remote push. Before reporting delivery,
12878
13748
  reconcile every requested item and observe it (\`workit verify-delivery\`).
@@ -12885,7 +13755,9 @@ PATH, \`npx -y @brainervirus/workit-cli\`). \`busy\` is retryable
12885
13755
  The task families (task, policy, evidence, finding, decision, worker, state)
12886
13756
  are optional continuity for tracked work; each branch has one implicit task
12887
13757
  that the first recording creates, and a solo edit needs no task. A decision
12888
- records a user choice; it never authorizes an effect.
13758
+ records a user choice; it never authorizes an effect. Judge work once with
13759
+ \`workit policy assess --judge risk= behavior= product-choice= plan=\`; an
13760
+ open choice or missing plan blocks edits until recorded.
12889
13761
  Omit revisions; a revision_conflict means one you passed is stale, so re-read.
12890
13762
  Across repositories, bind each item to its checkout, branch and endpoint,
12891
13763
  resolve competing targets before a mutation, and reconcile an uncertain
@@ -13023,7 +13895,7 @@ var exportDigest = (bundle) => sha256({
13023
13895
  task: bundle.task
13024
13896
  });
13025
13897
  // packages/workit-core/src/core/branch.ts
13026
- import { createHash as createHash7 } from "node:crypto";
13898
+ import { createHash as createHash9 } from "node:crypto";
13027
13899
 
13028
13900
  // packages/workit-core/src/core/git.ts
13029
13901
  import { execFileSync } from "node:child_process";
@@ -13097,14 +13969,14 @@ var gitContext = (workspaceRoot, paths = []) => {
13097
13969
  import {
13098
13970
  copyFileSync as copyFileSync2,
13099
13971
  cpSync as cpSync2,
13100
- existsSync as existsSync4,
13972
+ existsSync as existsSync5,
13101
13973
  mkdirSync as mkdirSync2,
13102
13974
  readFileSync as readFileSync3,
13103
13975
  readdirSync as readdirSync3,
13104
13976
  writeFileSync as writeFileSync2
13105
13977
  } from "node:fs";
13106
13978
  import os2 from "node:os";
13107
- import path10 from "node:path";
13979
+ import path12 from "node:path";
13108
13980
 
13109
13981
  // packages/workit-core/src/core/boundary.ts
13110
13982
  var EVENT = {
@@ -13148,7 +14020,7 @@ var mergePreset = (preset, input = {}, current = {
13148
14020
  protected: preset === "custom" ? input.protectedNames ?? current.branchPolicy.protected : [...defs.protected]
13149
14021
  };
13150
14022
  };
13151
- var resolveConfigDir = () => process.env.WORKFLOW_TOOLKIT_CONFIG ?? process.env.WORKFLOW_TOOLKIT_CONFIG_DIR ?? path10.join(process.env.XDG_CONFIG_HOME || path10.join(os2.homedir(), ".config"), "workit");
14023
+ var resolveConfigDir = () => process.env.WORKFLOW_TOOLKIT_CONFIG ?? process.env.WORKFLOW_TOOLKIT_CONFIG_DIR ?? path12.join(process.env.XDG_CONFIG_HOME || path12.join(os2.homedir(), ".config"), "workit");
13152
14024
  var migratedDir = null;
13153
14025
  var migrationFailed = false;
13154
14026
  var ensureConfigDir = (dir = resolveConfigDir()) => {
@@ -13158,12 +14030,12 @@ var ensureConfigDir = (dir = resolveConfigDir()) => {
13158
14030
  migratedDir = dir;
13159
14031
  return dir;
13160
14032
  }
13161
- const legacy = path10.join(process.env.XDG_CONFIG_HOME || path10.join(os2.homedir(), ".config"), "workflow-toolkit");
13162
- if (!existsSync4(legacy)) {
14033
+ const legacy = path12.join(process.env.XDG_CONFIG_HOME || path12.join(os2.homedir(), ".config"), "workflow-toolkit");
14034
+ if (!existsSync5(legacy)) {
13163
14035
  migratedDir = dir;
13164
14036
  return dir;
13165
14037
  }
13166
- if (!migrationFailed && existsSync4(dir)) {
14038
+ if (!migrationFailed && existsSync5(dir)) {
13167
14039
  migratedDir = dir;
13168
14040
  return dir;
13169
14041
  }
@@ -13171,9 +14043,9 @@ var ensureConfigDir = (dir = resolveConfigDir()) => {
13171
14043
  mkdirSync2(dir, { recursive: true });
13172
14044
  diagnosticLogger?.info(EVENT.migration, { from: legacy, to: dir });
13173
14045
  for (const entry of readdirSync3(legacy, { withFileTypes: true })) {
13174
- const src = path10.join(legacy, entry.name);
13175
- const dest = path10.join(dir, entry.name);
13176
- if (existsSync4(dest))
14046
+ const src = path12.join(legacy, entry.name);
14047
+ const dest = path12.join(dir, entry.name);
14048
+ if (existsSync5(dest))
13177
14049
  continue;
13178
14050
  try {
13179
14051
  if (entry.isDirectory())
@@ -13244,7 +14116,7 @@ var parseConfigResult = (raw, file) => {
13244
14116
  };
13245
14117
  };
13246
14118
  var readConfigTyped = (dir) => {
13247
- const file = path10.join(dir ?? configDir(), "config.json");
14119
+ const file = path12.join(dir ?? configDir(), "config.json");
13248
14120
  return parseConfigResult(readSafe(file), file);
13249
14121
  };
13250
14122
  var readConfig = () => {
@@ -13289,8 +14161,9 @@ var resolveCommitPolicy = (config, workspace) => {
13289
14161
  };
13290
14162
 
13291
14163
  // packages/workit-core/src/core/workspaces.ts
14164
+ import { createHash as createHash8 } from "node:crypto";
13292
14165
  import { readFileSync as readFileSync4, realpathSync as realpathSync3 } from "node:fs";
13293
- import path11 from "node:path";
14166
+ import path13 from "node:path";
13294
14167
  var nonBlank = string2().refine((value) => value.trim().length > 0, "must not be blank");
13295
14168
  var branchPreset = custom((value) => typeof value === "string" && Object.hasOwn(PRESETS, value), "unsupported branch preset");
13296
14169
  var commitPreset = custom((value) => typeof value === "string" && COMMIT_PRESETS.includes(value), "unsupported commit preset");
@@ -13343,7 +14216,7 @@ var releaseTrackSchema = object({
13343
14216
  context.addIssue({ code: "custom", path: [field], message: "entries must be unique" });
13344
14217
  }
13345
14218
  }
13346
- if (track.versionSource.kind === "package-json" && (path11.posix.isAbsolute(track.versionSource.path.replaceAll("\\", "/")) || /^[A-Za-z]:\//u.test(track.versionSource.path.replaceAll("\\", "/")) || track.versionSource.path.replaceAll("\\", "/").split("/").includes(".."))) {
14219
+ if (track.versionSource.kind === "package-json" && (path13.posix.isAbsolute(track.versionSource.path.replaceAll("\\", "/")) || /^[A-Za-z]:\//u.test(track.versionSource.path.replaceAll("\\", "/")) || track.versionSource.path.replaceAll("\\", "/").split("/").includes(".."))) {
13347
14220
  context.addIssue({
13348
14221
  code: "custom",
13349
14222
  path: ["versionSource", "path"],
@@ -13362,6 +14235,7 @@ var workspaceConfigSchema = object({
13362
14235
  }).passthrough().optional(),
13363
14236
  autoApprove: union([boolean2(), array(nonBlank)]).optional(),
13364
14237
  defaultEndpoint: _enum(["commit", "pr"]).optional(),
14238
+ verification: _enum(["self", "independent"]).optional(),
13365
14239
  autonomy: object(Object.fromEntries(["push", "pr", "merge", "release", "rerun"].map((kind) => [
13366
14240
  kind,
13367
14241
  union([boolean2(), literal("verified")]).optional()
@@ -13402,6 +14276,72 @@ var workspacesFileSchema = object({ workspaces: array(workspaceConfigSchema).opt
13402
14276
  names.add(workspace.name);
13403
14277
  }
13404
14278
  });
14279
+ var workspacesRevision = (contents) => createHash8("sha256").update(contents ?? "<missing>").digest("hex");
14280
+ var validateWorkspacesDocument = (parsed, file) => {
14281
+ if (!isConfigObject(parsed)) {
14282
+ return { status: "malformed", path: file, entries: [], error: `${file} is not a JSON object` };
14283
+ }
14284
+ const validated = workspacesFileSchema.safeParse(parsed, { reportInput: true });
14285
+ if (!validated.success) {
14286
+ const detail = validated.error.issues.map((issue) => {
14287
+ const field = String(issue.path[issue.path.length - 1] ?? "");
14288
+ const attempted = ["provider", "preset", "strategy"].includes(field) && typeof issue.input === "string" ? ` (${JSON.stringify(issue.input)})` : "";
14289
+ return `${issue.path.map(String).join(".") || "workspaces"}: ${issue.message}${attempted}`;
14290
+ }).join("; ");
14291
+ return {
14292
+ status: "invalid",
14293
+ path: file,
14294
+ entries: [],
14295
+ error: `${file} has invalid workspace configuration: ${detail}`
14296
+ };
14297
+ }
14298
+ for (const [index, workspace] of (validated.data.workspaces ?? []).entries()) {
14299
+ const glob = validateWorkspaceGlob(workspace.glob);
14300
+ if (!glob.ok) {
14301
+ return {
14302
+ status: "invalid",
14303
+ path: file,
14304
+ entries: [],
14305
+ error: `${file} workspaces.${index}.glob: ${glob.error}`
14306
+ };
14307
+ }
14308
+ }
14309
+ return {
14310
+ status: "valid",
14311
+ path: file,
14312
+ entries: validated.data.workspaces ?? [],
14313
+ document: parsed
14314
+ };
14315
+ };
14316
+ var readWorkspacesResult = (dir = configDir()) => {
14317
+ const file = path13.join(dir, "workspaces.json");
14318
+ let raw;
14319
+ try {
14320
+ raw = readFileSync4(file, "utf8");
14321
+ } catch (error) {
14322
+ if (error.code === "ENOENT")
14323
+ return { status: "missing", path: file, entries: [], revision: workspacesRevision(null) };
14324
+ return {
14325
+ status: "malformed",
14326
+ path: file,
14327
+ entries: [],
14328
+ error: `${file} could not be read: ${error instanceof Error ? error.message : String(error)}`
14329
+ };
14330
+ }
14331
+ let parsed;
14332
+ try {
14333
+ parsed = JSON.parse(raw);
14334
+ } catch {
14335
+ return {
14336
+ status: "malformed",
14337
+ path: file,
14338
+ entries: [],
14339
+ revision: workspacesRevision(raw),
14340
+ error: `${file} is not valid JSON`
14341
+ };
14342
+ }
14343
+ return { ...validateWorkspacesDocument(parsed, file), revision: workspacesRevision(raw) };
14344
+ };
13405
14345
  var globToRegExp = (glob) => {
13406
14346
  let out = "";
13407
14347
  for (let i = 0;i < glob.length; i++) {
@@ -13499,6 +14439,30 @@ var selectWorkspaceMatch = (matches, cwd, workspaceName) => {
13499
14439
  }
13500
14440
  return mostSpecific[0];
13501
14441
  };
14442
+ var resolveWorkspaceFromEntries = (cwd, entries, workspaceName) => {
14443
+ const targets = [cwd, realpathOf(cwd)].map((p) => p.replaceAll("\\", "/"));
14444
+ const matches = [];
14445
+ for (const ws of entries) {
14446
+ const glob = ws.glob.replaceAll("\\", "/");
14447
+ const canonical = canonicalGlob(glob);
14448
+ let matched = false;
14449
+ for (const target of targets) {
14450
+ if (matchWorkspace(glob, target) || canonical !== glob && matchWorkspace(canonical, target)) {
14451
+ matched = true;
14452
+ break;
14453
+ }
14454
+ }
14455
+ if (matched)
14456
+ matches.push(ws);
14457
+ }
14458
+ return selectWorkspaceMatch(matches, cwd, workspaceName) ?? null;
14459
+ };
14460
+ var resolveWorkspaceFrom = (cwd, dir, workspaceName) => {
14461
+ const result = readWorkspacesResult(dir);
14462
+ if (result.status === "malformed" || result.status === "invalid")
14463
+ throw new Error(result.error);
14464
+ return resolveWorkspaceFromEntries(cwd, result.entries, workspaceName);
14465
+ };
13502
14466
  var runtimeWorkspacePolicySchema = (kind) => {
13503
14467
  const key = kind === "branch" ? "branchPolicy" : kind === "commit" ? "commitPolicy" : "vcs";
13504
14468
  const policySchema = kind === "branch" ? workspaceBranchPolicySchema : kind === "commit" ? workspaceCommitPolicySchema : object({
@@ -13521,7 +14485,7 @@ var runtimeWorkspaceIndexSchema = object({
13521
14485
  workspaces: array(object({ name: nonBlank, glob: nonBlank }).passthrough()).optional()
13522
14486
  }).passthrough();
13523
14487
  var resolveRuntimeWorkspaceCandidate = (cwd, kind) => {
13524
- const file = path11.join(configDir(), "workspaces.json");
14488
+ const file = path13.join(configDir(), "workspaces.json");
13525
14489
  let raw;
13526
14490
  try {
13527
14491
  raw = readFileSync4(file, "utf8");
@@ -13584,13 +14548,13 @@ var resolveRuntimeWorkspacePolicy = (cwd, kind, requestedProfile) => {
13584
14548
  };
13585
14549
 
13586
14550
  // packages/workit-core/src/core/vcs-config.ts
13587
- import fs11 from "node:fs";
13588
- import path12 from "node:path";
13589
- import { spawnSync as spawnSync6 } from "node:child_process";
13590
- var vcsConfigPath = () => process.env.WORKFLOW_VCS_CONFIG ?? path12.join(configDir(), "vcs.json");
14551
+ import fs12 from "node:fs";
14552
+ import path14 from "node:path";
14553
+ import { spawnSync as spawnSync7 } from "node:child_process";
14554
+ var vcsConfigPath = () => process.env.WORKFLOW_VCS_CONFIG ?? path14.join(configDir(), "vcs.json");
13591
14555
  var vcsCwd = (cwd) => cwd ?? process.env.WORKFLOW_WORKSPACE_ROOT ?? process.cwd();
13592
14556
  var remoteProvider = (cwd) => {
13593
- const r = spawnSync6("git", ["remote", "get-url", "origin"], { cwd, encoding: "utf8" });
14557
+ const r = spawnSync7("git", ["remote", "get-url", "origin"], { cwd, encoding: "utf8" });
13594
14558
  if (r.status !== 0)
13595
14559
  return null;
13596
14560
  const url = (r.stdout ?? "").trim();
@@ -13627,7 +14591,7 @@ var remoteEndpoint = (remote) => {
13627
14591
  var configuredSshEndpoint = (host) => {
13628
14592
  if (host.startsWith("-"))
13629
14593
  return null;
13630
- const resolved = spawnSync6("ssh", ["-G", host], {
14594
+ const resolved = spawnSync7("ssh", ["-G", host], {
13631
14595
  encoding: "utf8",
13632
14596
  timeout: 5000,
13633
14597
  maxBuffer: 1024 * 1024,
@@ -13677,7 +14641,7 @@ var readVcsConfig = () => {
13677
14641
  const file = vcsConfigPath();
13678
14642
  let raw;
13679
14643
  try {
13680
- raw = fs11.readFileSync(file, "utf8");
14644
+ raw = fs12.readFileSync(file, "utf8");
13681
14645
  } catch {
13682
14646
  return { status: "missing", path: file, config: {} };
13683
14647
  }
@@ -13743,7 +14707,7 @@ function vcsConfig(mode, cwd) {
13743
14707
  const prov = cfg[provider] ?? {};
13744
14708
  const out = {
13745
14709
  ok: true,
13746
- configPath: path12.resolve(cfgPath),
14710
+ configPath: path14.resolve(cfgPath),
13747
14711
  provider,
13748
14712
  defaultTargetBranch: defaultTarget,
13749
14713
  pr: cfg.pr ?? {},
@@ -13781,7 +14745,7 @@ function mergedPrStyle(limit = 6, cwd) {
13781
14745
  `).filter((l) => l.startsWith("## ")).map((l) => l.trim()),
13782
14746
  descriptionPreview: desc.slice(0, 600)
13783
14747
  });
13784
- const remoteResult = spawnSync6("git", ["remote", "get-url", "--push", "--all", "origin"], {
14748
+ const remoteResult = spawnSync7("git", ["remote", "get-url", "--push", "--all", "origin"], {
13785
14749
  cwd,
13786
14750
  encoding: "utf8"
13787
14751
  });
@@ -13797,7 +14761,7 @@ function mergedPrStyle(limit = 6, cwd) {
13797
14761
  if (!hostingApiHostMatches(remote, host))
13798
14762
  return { ok: false, error: "PR style target does not match the provider host" };
13799
14763
  if (provider === "gitlab") {
13800
- const run = (args) => spawnSync6("glab", ["api", ...args], {
14764
+ const run = (args) => spawnSync7("glab", ["api", ...args], {
13801
14765
  cwd,
13802
14766
  encoding: "utf8",
13803
14767
  env
@@ -13817,7 +14781,7 @@ function mergedPrStyle(limit = 6, cwd) {
13817
14781
  });
13818
14782
  }
13819
14783
  } else if (provider === "github") {
13820
- const r = spawnSync6("gh", [
14784
+ const r = spawnSync7("gh", [
13821
14785
  "pr",
13822
14786
  "list",
13823
14787
  "--repo",
@@ -13893,7 +14857,7 @@ var branchPolicySnapshotFor = (workspaceRoot) => {
13893
14857
  const source = resolved.provenance.branchPolicy;
13894
14858
  const allowed = branchPolicy.allowed.map((pattern) => `${pattern.source}/${pattern.flags}`).toSorted();
13895
14859
  const protectedRefs = [...branchPolicy.protected].map((name) => name.toLowerCase()).toSorted();
13896
- const fingerprint = createHash7("sha256").update(JSON.stringify({ allowed, protectedRefs, source })).digest("hex");
14860
+ const fingerprint = createHash9("sha256").update(JSON.stringify({ allowed, protectedRefs, source })).digest("hex");
13897
14861
  return {
13898
14862
  branchPolicy,
13899
14863
  namePolicy: { allowed: branchPolicy.allowed, protected: branchPolicy.protected },
@@ -13938,305 +14902,34 @@ var shellBranchPolicyViolation = (root, command) => {
13938
14902
  import { realpathSync as realpathSync4 } from "node:fs";
13939
14903
 
13940
14904
  // packages/workit-core/src/core/policy-resolver.ts
13941
- var resolverInputSchema = object({
13942
- intent: intentSchema,
13943
- assessment: assessmentSchema,
13944
- constraints: array(constraintSchema),
13945
- prior: object({
13946
- decisions: array(decisionSchema),
13947
- findings: array(findingSchema),
13948
- requirements: array(requirementSchema)
13949
- }).strict(),
13950
- preferences: object({ fast: boolean2().optional(), thorough: boolean2().optional() }).strict().optional()
13951
- }).strict();
13952
- var compareCodeUnits3 = (left, right) => {
13953
- const length = Math.min(left.length, right.length);
13954
- for (let index = 0;index < length; index += 1) {
13955
- const difference = left.charCodeAt(index) - right.charCodeAt(index);
13956
- if (difference !== 0)
13957
- return difference;
13958
- }
13959
- return left.length - right.length;
13960
- };
13961
- var stableUnique = (values) => [...new Set(values)].toSorted(compareCodeUnits3);
13962
- var stableList = (values) => [...values].toSorted((left, right) => compareCodeUnits3(canonicalJson(left), canonicalJson(right)));
13963
- var normalizedScope = (scope) => ({
13964
- description: scope.description,
13965
- paths: stableUnique(scope.paths),
13966
- exclusions: stableUnique(scope.exclusions)
13967
- });
13968
- var normalizedRefList = (values) => stableList(values);
13969
- var normalizeAssessment = (assessment) => ({
13970
- facts: stableList(assessment.facts.map((fact) => ({ ...fact, refs: normalizedRefList(fact.refs) }))),
13971
- signals: Object.fromEntries(Object.entries(assessment.signals).map(([name, signal]) => [
13972
- name,
13973
- { ...signal, refs: normalizedRefList(signal.refs) }
13974
- ])),
13975
- consequences: stableList(assessment.consequences.map((consequence) => ({
13976
- ...consequence,
13977
- fact: { ...consequence.fact, refs: normalizedRefList(consequence.fact.refs) }
13978
- }))),
13979
- verification: stableList(assessment.verification.map((verification) => ({
13980
- ...verification,
13981
- scope: normalizedScope(verification.scope),
13982
- availableChecks: normalizedRefList(verification.availableChecks),
13983
- gaps: stableUnique(verification.gaps)
13984
- })))
13985
- });
13986
- var normalizeConstraint = (constraint) => ({
13987
- ...constraint,
13988
- source: constraint.source,
13989
- requires: stableList(constraint.requires.map((requirement) => ({
13990
- ...requirement,
13991
- scope: normalizedScope(requirement.scope),
13992
- refs: normalizedRefList(requirement.refs)
13993
- })))
13994
- });
13995
- var normalizeDecision = (decision) => ({
13996
- ...decision,
13997
- binding: {
13998
- ...decision.binding,
13999
- scope: normalizedScope(decision.binding.scope),
14000
- contentRefs: normalizedRefList(decision.binding.contentRefs)
14001
- },
14002
- requirementIds: stableUnique(decision.requirementIds)
14003
- });
14004
- var normalizeIntent = (intent) => ({
14005
- ...intent,
14006
- scope: normalizedScope(intent.scope),
14007
- authorityRefs: normalizedRefList(intent.authorityRefs)
14008
- });
14009
- var normalizePrior = (prior) => ({
14010
- decisions: stableList(prior.decisions.map(normalizeDecision)),
14011
- findings: stableList(prior.findings.map((finding) => ({
14012
- ...finding,
14013
- scope: normalizedScope(finding.scope),
14014
- refs: normalizedRefList(finding.refs),
14015
- resolution: finding.resolution ? {
14016
- ...finding.resolution,
14017
- evidenceIds: stableUnique(finding.resolution.evidenceIds),
14018
- decisionIds: stableUnique(finding.resolution.decisionIds)
14019
- } : null
14020
- }))),
14021
- requirements: stableList(prior.requirements.map((requirement) => ({
14022
- ...requirement,
14023
- scope: normalizedScope(requirement.scope)
14024
- })))
14025
- });
14026
- var invalidFields = (error) => ({
14027
- fields: error.issues.map((issue) => ({
14028
- path: issue.code === "unrecognized_keys" ? issue.keys.join(".") : issue.path.join("."),
14029
- reason: issue.message
14030
- }))
14031
- });
14032
- function normalizeResolverInput(input) {
14033
- const parsed = resolverInputSchema.safeParse(input);
14034
- if (!parsed.success)
14035
- return failure2("invalid_input", "resolver input is invalid", invalidFields(parsed.error));
14036
- const value = parsed.data;
14037
- const signals = value.assessment.signals;
14038
- if (signals.behaviorChange.value === true && signals.mechanicalLowRisk.value === true)
14039
- return failure2("invalid_input", "behavioral change cannot be wholly mechanical", {
14040
- fields: [
14041
- {
14042
- path: "assessment.signals",
14043
- reason: "contradictory behaviorChange and mechanicalLowRisk"
14044
- }
14045
- ]
14046
- });
14047
- const ids = value.constraints.map((constraint) => constraint.id);
14905
+ function resolvePolicy(input) {
14906
+ const ids = input.constraints.map((constraint) => constraint.id);
14048
14907
  if (new Set(ids).size !== ids.length)
14049
14908
  return failure2("invalid_input", "constraint IDs must be unique", {
14050
14909
  fields: [{ path: "constraints", reason: "duplicate constraint id" }]
14051
14910
  });
14052
- if (value.preferences?.fast && value.preferences.thorough)
14053
- return failure2("invalid_input", "fast and thorough preferences contradict", {
14054
- fields: [{ path: "preferences", reason: "fast and thorough cannot both be enabled" }]
14055
- });
14056
- const normalized = {
14057
- intent: normalizeIntent(value.intent),
14058
- assessment: normalizeAssessment(value.assessment),
14059
- constraints: stableList(value.constraints.map(normalizeConstraint)),
14060
- prior: normalizePrior(value.prior),
14061
- ...value.preferences ? { preferences: { ...value.preferences } } : {},
14062
- policyVersion: POLICY_VERSION
14063
- };
14064
- return success(null, null, normalized);
14065
- }
14066
- var requirement = (spec) => ({
14067
- ...spec,
14068
- id: requirementId({
14069
- ruleId: spec.ruleId,
14070
- scope: spec.scope,
14071
- satisfaction: spec.satisfaction,
14072
- before: spec.before,
14073
- dependentAction: spec.dependentAction
14074
- })
14075
- });
14076
- var signalReason = (input, name) => input.assessment.signals[name].reason;
14077
- function resolveRequirements(input) {
14078
- const { signals } = input.assessment;
14079
- const scope = input.intent.scope;
14080
- const requirements = [];
14081
- const hasUnknownSignal = Object.values(signals).some((signal) => signal.value === "unknown");
14082
- const hasUnknownFact = [
14083
- ...input.assessment.facts,
14084
- ...input.assessment.consequences.map(({ fact }) => fact)
14085
- ].some((fact) => fact.basis === "unknown");
14086
- if (hasUnknownSignal || hasUnknownFact)
14087
- requirements.push(requirement({
14088
- ruleId: "consequential-unknown",
14089
- dimension: "investigation",
14090
- scope,
14091
- reason: `Resolve consequential uncertainty before the dependent action: ${[
14092
- ...Object.entries(signals).filter(([, signal]) => signal.value === "unknown").map(([name]) => name),
14093
- ...hasUnknownFact ? ["unknown fact"] : []
14094
- ].join(", ")}`,
14095
- satisfaction: "An observed or inferred fact resolves the uncertainty and records its supporting references.",
14096
- before: "dependent_action",
14097
- dependentAction: null,
14098
- acceptanceAllowed: false
14099
- }));
14100
- if (signals.productChoiceOpen.value === true)
14101
- requirements.push(requirement({
14102
- ruleId: "product-decision",
14103
- dimension: "decisions",
14104
- scope,
14105
- reason: `A consequential product choice remains open: ${signalReason(input, "productChoiceOpen")}`,
14106
- satisfaction: "Present the material alternatives and record the user's decision before the dependent action.",
14107
- before: "dependent_action",
14108
- dependentAction: null,
14109
- acceptanceAllowed: false
14110
- }));
14111
- const independentBehaviorReview = input.preferences?.thorough === true || input.assessment.consequences.some((consequence) => ["security", "data", "public_contract", "operations"].includes(consequence.area));
14112
- if (signals.behaviorChange.value === true)
14113
- requirements.push(requirement({
14114
- ruleId: "behavioral-verification",
14115
- dimension: "testing",
14116
- scope,
14117
- reason: `Behavior, side effects, permissions, or data handling may change: ${signalReason(input, "behaviorChange")}`,
14118
- satisfaction: "A useful behavioral check passes against the affected behavior and current candidate.",
14119
- before: "close",
14120
- dependentAction: null,
14121
- acceptanceAllowed: false
14122
- }), requirement({
14123
- ruleId: independentBehaviorReview ? "fresh-context-review" : "self-review",
14124
- dimension: "review",
14125
- scope,
14126
- reason: independentBehaviorReview ? "Consequential behavior or the thorough preference requires fresh-context review." : "Bounded behavior changes use self-review unless consequential risks or trusted rules require more.",
14127
- satisfaction: independentBehaviorReview ? "A separate review context examines the current candidate and records its result." : "The lead reviews the resulting diff and records the relevant scope and checks.",
14128
- before: "close",
14129
- dependentAction: null,
14130
- acceptanceAllowed: !independentBehaviorReview
14131
- }));
14132
- else if (signals.mechanicalLowRisk.value === true)
14133
- requirements.push(requirement({
14134
- ruleId: "mechanical-existing-checks",
14135
- dimension: "verification",
14136
- scope,
14137
- reason: `The change is demonstrably mechanical and low-risk: ${signalReason(input, "mechanicalLowRisk")}`,
14138
- satisfaction: "Run the relevant existing checks for affected consumers; do not add tautological tests.",
14139
- before: "close",
14140
- dependentAction: null,
14141
- acceptanceAllowed: true
14142
- }), requirement({
14143
- ruleId: input.preferences?.thorough ? "fresh-context-review" : "self-review",
14144
- dimension: "review",
14145
- scope,
14146
- reason: input.preferences?.thorough ? "The thorough preference upgrades mechanical self-review to fresh-context review." : "Mechanical low-risk work uses self-review rather than mandatory fresh-context review.",
14147
- satisfaction: input.preferences?.thorough ? "A separate review context examines the current candidate and records its result." : "The lead reviews the resulting diff and records the relevant scope and checks.",
14148
- before: "close",
14149
- dependentAction: null,
14150
- acceptanceAllowed: !input.preferences?.thorough
14151
- }));
14152
- if (signals.behaviorChange.value === true || signals.mechanicalLowRisk.value === true)
14153
- requirements.push(requirement({
14154
- ruleId: "pre-pr-cleanup",
14155
- dimension: "verification",
14156
- scope,
14157
- reason: "Implementation output must be deslopped before it is delivered.",
14158
- satisfaction: "Run the workit-deslop pass and record passing check evidence, or record an approved limitation decision waiving cleanup.",
14159
- before: "dependent_action",
14160
- dependentAction: "hosting.pull_request",
14161
- acceptanceAllowed: true
14162
- }));
14163
- if (signals.durableAgreementNeeded.value === true)
14164
- requirements.push(requirement({
14165
- ruleId: "durable-spec",
14166
- dimension: "artifacts",
14167
- scope,
14168
- reason: `The work needs a durable agreement about intended behavior: ${signalReason(input, "durableAgreementNeeded")}`,
14169
- satisfaction: "Record the agreed behavior in a durable specification before implementation writes.",
14170
- before: "write",
14171
- dependentAction: null,
14172
- acceptanceAllowed: true
14173
- }));
14174
- if (signals.coordinationPlanNeeded.value === true)
14175
- requirements.push(requirement({
14176
- ruleId: "coordination-plan",
14177
- dimension: "continuity",
14178
- scope,
14179
- reason: `Dependencies, sequencing, or resumption need durable coordination: ${signalReason(input, "coordinationPlanNeeded")}`,
14180
- satisfaction: "Record useful sequencing, dependencies, and the next action in a compact plan.",
14181
- before: "dependent_action",
14182
- dependentAction: null,
14183
- acceptanceAllowed: true
14184
- }));
14185
- if (signals.helperUseful.value === true && !input.preferences?.fast)
14186
- requirements.push(requirement({
14187
- ruleId: "helper-usefulness",
14188
- dimension: "delegation",
14189
- scope,
14190
- reason: `Independent judgment or investigation justifies a scoped helper: ${signalReason(input, "helperUseful")}`,
14191
- satisfaction: "Use a bounded helper only for the independent objective and reconcile its result.",
14192
- before: "dependent_action",
14193
- dependentAction: null,
14194
- acceptanceAllowed: true
14195
- }));
14196
- for (const constraint of input.constraints)
14197
- for (const obligation of constraint.requires) {
14198
- const dimension = obligation.method ?? (obligation.kind === "check" ? "verification" : obligation.kind === "decision" ? "decisions" : "challenge");
14199
- requirements.push(requirement({
14200
- ruleId: `project-constraint:${constraint.id}:${obligation.kind}:${sha256({
14201
- kind: obligation.kind,
14202
- scope: normalizedScope(obligation.scope),
14203
- refs: normalizedRefList(obligation.refs),
14204
- method: obligation.method,
14205
- before: obligation.before,
14206
- dependentAction: obligation.dependentAction
14207
- })}`,
14208
- dimension,
14209
- scope: obligation.scope,
14210
- reason: `${constraint.kind} constraint requires ${obligation.kind}: ${constraint.statement}`,
14211
- satisfaction: `Satisfy the constraint obligation using its referenced ${obligation.kind}.`,
14212
- before: obligation.before,
14213
- dependentAction: obligation.dependentAction,
14214
- acceptanceAllowed: constraint.acceptanceAllowed && obligation.kind !== "decision"
14215
- }));
14216
- }
14217
- const seen = new Set;
14218
- return requirements.filter((item) => {
14219
- if (seen.has(item.id))
14220
- return false;
14221
- seen.add(item.id);
14222
- return true;
14223
- });
14224
- }
14225
- function resolvePolicy(input) {
14226
- const normalized = normalizeResolverInput(input);
14227
- if (!normalized.ok)
14228
- return normalized;
14229
- const requirements = resolveRequirements(normalized.data);
14230
14911
  const policy = {
14231
14912
  policyVersion: POLICY_VERSION,
14232
- inputDigest: sha256(canonicalJson(normalized.data)),
14233
- requirements
14913
+ inputDigest: sha256(canonicalJson({
14914
+ judgment: input.judgment,
14915
+ scope: input.intent.scope,
14916
+ constraints: input.constraints,
14917
+ verification: input.verification ?? "self",
14918
+ policyVersion: POLICY_VERSION
14919
+ })),
14920
+ requirements: deriveRequirements(input.judgment, input.intent.scope, input.constraints, input.verification)
14234
14921
  };
14235
14922
  const parsed = policySchema.safeParse(policy);
14236
14923
  if (!parsed.success)
14237
- return failure2("invalid_input", "resolved policy is invalid", invalidFields(parsed.error));
14924
+ return failure2("invalid_input", "resolved policy is invalid", {
14925
+ fields: parsed.error.issues.map((issue) => ({
14926
+ path: issue.path.join("."),
14927
+ reason: issue.message
14928
+ }))
14929
+ });
14238
14930
  return success(null, null, parsed.data);
14239
14931
  }
14932
+ var compareCodeUnits3 = (left, right) => left < right ? -1 : left > right ? 1 : 0;
14240
14933
  function diffPolicy(previous, next, reason, now) {
14241
14934
  if (!policySchema.safeParse(previous).success && previous !== null)
14242
14935
  throw new TypeError("previous policy is invalid");
@@ -14264,7 +14957,222 @@ function diffPolicy(previous, next, reason, now) {
14264
14957
  return parsed.data;
14265
14958
  }
14266
14959
 
14960
+ // packages/workit-core/src/core/policy/judgment.ts
14961
+ var isObject6 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
14962
+ var squash = (key) => key.toLowerCase().replace(/[_-]/g, "");
14963
+ var ALIASES = {
14964
+ riskTier: ["risktier", "risk", "risklevel", "tier"],
14965
+ behaviorChange: ["behaviorchange", "behavior", "behaviour", "behaviourchange"],
14966
+ productChoiceOpen: ["productchoiceopen", "openproductchoice", "productchoice", "openchoice"],
14967
+ needsPlan: ["needsplan", "plan", "needsspec", "spec"],
14968
+ note: ["note", "notes", "reason", "why"],
14969
+ refs: ["refs", "ref", "references"]
14970
+ };
14971
+ var LEGACY_KEYS = new Set(["assessment", "facts", "signals", "consequences", "verification"]);
14972
+ var RISK = {
14973
+ trivial: "trivial",
14974
+ low: "trivial",
14975
+ minor: "trivial",
14976
+ mechanical: "trivial",
14977
+ none: "trivial",
14978
+ normal: "normal",
14979
+ medium: "normal",
14980
+ med: "normal",
14981
+ moderate: "normal",
14982
+ high: "high",
14983
+ critical: "high",
14984
+ major: "high"
14985
+ };
14986
+ var YES = new Set(["true", "yes", "y", "1", "on"]);
14987
+ var NO = new Set(["false", "no", "n", "0", "off", "none"]);
14988
+ var flag = (value, unknownAs) => {
14989
+ if (typeof value === "boolean")
14990
+ return { ok: true, value };
14991
+ if (typeof value === "number" && (value === 0 || value === 1))
14992
+ return { ok: true, value: value === 1 };
14993
+ if (typeof value !== "string")
14994
+ return { ok: false };
14995
+ const text = value.trim().toLowerCase();
14996
+ if (YES.has(text))
14997
+ return { ok: true, value: true };
14998
+ if (NO.has(text))
14999
+ return { ok: true, value: false };
15000
+ if (text === "unknown" || text === "maybe")
15001
+ return { ok: true, value: unknownAs };
15002
+ return { ok: false };
15003
+ };
15004
+ var fromLegacy = (raw, previous) => {
15005
+ const source = isObject6(raw.assessment) ? raw.assessment : raw;
15006
+ const signals = isObject6(source.signals) ? source.signals : {};
15007
+ const value = (name) => isObject6(signals[name]) ? signals[name].value : undefined;
15008
+ const known = (name, fallback) => typeof value(name) === "boolean" ? value(name) : fallback;
15009
+ const behaviorChange = known("behaviorChange", previous?.behaviorChange ?? false);
15010
+ const consequences = Array.isArray(source.consequences) ? source.consequences : [];
15011
+ const freshReview = value("behaviorChange") === true && consequences.some((item) => isObject6(item) && ["security", "data", "public_contract", "operations"].includes(String(item.area)));
15012
+ return {
15013
+ riskTier: freshReview ? "high" : value("mechanicalLowRisk") === true && !behaviorChange ? "trivial" : "normal",
15014
+ behaviorChange,
15015
+ productChoiceOpen: known("productChoiceOpen", previous?.productChoiceOpen ?? false),
15016
+ needsPlan: known("durableAgreementNeeded", previous?.needsPlan ?? false) || value("coordinationPlanNeeded") === true
15017
+ };
15018
+ };
15019
+ var DEFAULT = {
15020
+ riskTier: "normal",
15021
+ behaviorChange: false,
15022
+ productChoiceOpen: false,
15023
+ needsPlan: false,
15024
+ note: null,
15025
+ refs: []
15026
+ };
15027
+ function normalizeJudgment(raw, previous) {
15028
+ const input = isObject6(raw) ? raw : {};
15029
+ const legacy = Object.keys(input).some((key) => LEGACY_KEYS.has(key));
15030
+ const base = { ...DEFAULT, ...previous, refs: [...previous?.refs ?? []] };
15031
+ const judgment = legacy ? { ...base, ...fromLegacy(input, previous) } : { ...base };
15032
+ const ignored = [];
15033
+ const fields = [];
15034
+ for (const [key, value] of Object.entries(input)) {
15035
+ if (LEGACY_KEYS.has(key))
15036
+ continue;
15037
+ const name = Object.keys(ALIASES).find((field) => ALIASES[field].includes(squash(key)));
15038
+ if (!name) {
15039
+ ignored.push(key);
15040
+ continue;
15041
+ }
15042
+ if (name === "riskTier") {
15043
+ const tier = typeof value === "string" ? RISK[value.trim().toLowerCase()] : undefined;
15044
+ if (tier)
15045
+ judgment.riskTier = tier;
15046
+ else
15047
+ fields.push({ path: key, reason: "expected trivial|normal|high (or low|medium)" });
15048
+ } else if (name === "note") {
15049
+ judgment.note = typeof value === "string" && value.trim() ? value.trim() : null;
15050
+ } else if (name === "refs") {
15051
+ const items = Array.isArray(value) ? value : [value];
15052
+ judgment.refs.push(...items.filter((item) => item !== null && item !== "").map(refOf));
15053
+ } else {
15054
+ if (name === "needsPlan" && typeof value === "string" && !flag(value, false).ok && value.trim()) {
15055
+ judgment.needsPlan = true;
15056
+ judgment.refs.push(refOf(value.trim()));
15057
+ continue;
15058
+ }
15059
+ const parsed = flag(value, name === "behaviorChange");
15060
+ if (parsed.ok)
15061
+ judgment[name] = parsed.value;
15062
+ else
15063
+ fields.push({ path: key, reason: "expected true|false (yes|no accepted)" });
15064
+ }
15065
+ }
15066
+ if (fields.length)
15067
+ return failure2("invalid_input", "judgment values are invalid", { fields });
15068
+ const seen = new Set;
15069
+ judgment.refs = judgment.refs.filter((ref) => {
15070
+ const key = JSON.stringify(ref);
15071
+ return seen.has(key) ? false : (seen.add(key), true);
15072
+ });
15073
+ const parsed = judgmentSchema.safeParse(judgment);
15074
+ if (!parsed.success)
15075
+ return failure2("invalid_input", "judgment is invalid", {
15076
+ fields: parsed.error.issues.map((issue) => ({
15077
+ path: issue.path.join("."),
15078
+ reason: issue.message
15079
+ }))
15080
+ });
15081
+ return success(null, null, { judgment: parsed.data, ignored, legacy });
15082
+ }
15083
+
15084
+ // packages/workit-core/src/autonomy.ts
15085
+ import os3 from "node:os";
15086
+ import path15 from "node:path";
15087
+ var accountHome = () => {
15088
+ try {
15089
+ return os3.userInfo().homedir;
15090
+ } catch {
15091
+ return os3.homedir();
15092
+ }
15093
+ };
15094
+ var grantsHome = { resolve: accountHome };
15095
+ var grantsDir = () => path15.join(grantsHome.resolve(), ".config", "workit");
15096
+ var grantsOverride = () => {
15097
+ const configured = path15.resolve(resolveConfigDir());
15098
+ return configured === path15.resolve(grantsDir()) ? null : `the config directory is redirected to ${configured} (WORKFLOW_TOOLKIT_CONFIG, WORKFLOW_TOOLKIT_CONFIG_DIR or XDG_CONFIG_HOME); grants are read only from ${path15.join(grantsDir(), "workspaces.json")}, so the D4 defaults apply`;
15099
+ };
15100
+ var GRANT_KINDS = ["push", "pr", "merge", "release", "rerun"];
15101
+ var DEFAULT_GRANTS = Object.freeze({
15102
+ push: true,
15103
+ pr: true,
15104
+ merge: false,
15105
+ release: false,
15106
+ rerun: true
15107
+ });
15108
+ var DEFAULT_ENDPOINT = "commit";
15109
+ var DEFAULT_VERIFICATION = "self";
15110
+ var verificationOf = (entry) => entry?.verification === "independent" ? "independent" : "self";
15111
+ var endpointOf = (entry) => entry?.defaultEndpoint === "pr" ? "pr" : "commit";
15112
+ var isGrantValue = (value) => value === true || value === false || value === "verified";
15113
+ var normalized = (kind, value) => value === "verified" && kind !== "merge" ? true : value;
15114
+ var fromAutoApprove = (value) => {
15115
+ if (value === true)
15116
+ return { push: true, pr: true, merge: "verified" };
15117
+ if (!Array.isArray(value))
15118
+ return null;
15119
+ const grants = {};
15120
+ for (const item of value)
15121
+ if (item === "push" || item === "pr")
15122
+ grants[item] = true;
15123
+ else if (item === "merge")
15124
+ grants.merge = "verified";
15125
+ return Object.keys(grants).length > 0 ? grants : null;
15126
+ };
15127
+ function configuredGrants(entry) {
15128
+ const record = entry ?? {};
15129
+ const raw = record.autonomy;
15130
+ if (raw && typeof raw === "object" && !Array.isArray(raw)) {
15131
+ const grants = {};
15132
+ for (const kind of GRANT_KINDS) {
15133
+ const value = raw[kind];
15134
+ if (isGrantValue(value))
15135
+ grants[kind] = normalized(kind, value);
15136
+ }
15137
+ return { grants, source: "autonomy" };
15138
+ }
15139
+ const legacy = fromAutoApprove(record.autoApprove);
15140
+ return legacy ? { grants: legacy, source: "autoApprove" } : { grants: {}, source: "default" };
15141
+ }
15142
+ function resolveAutonomy(cwd) {
15143
+ const override = grantsOverride();
15144
+ if (override)
15145
+ return {
15146
+ workspace: null,
15147
+ grants: { ...DEFAULT_GRANTS },
15148
+ configured: [],
15149
+ source: "default",
15150
+ accountConfigured: false,
15151
+ defaultEndpoint: DEFAULT_ENDPOINT,
15152
+ verification: DEFAULT_VERIFICATION,
15153
+ note: override
15154
+ };
15155
+ const workspace = resolveWorkspaceFrom(cwd, grantsDir(), process.env.WORKFLOW_WORKSPACE_NAME?.trim() || undefined);
15156
+ const { grants, source } = configuredGrants(workspace);
15157
+ return {
15158
+ workspace: workspace?.name ?? null,
15159
+ grants: { ...DEFAULT_GRANTS, ...grants },
15160
+ configured: GRANT_KINDS.filter((kind) => grants[kind] !== undefined),
15161
+ source,
15162
+ accountConfigured: Boolean(workspace?.vcs?.account),
15163
+ defaultEndpoint: endpointOf(workspace),
15164
+ verification: verificationOf(workspace)
15165
+ };
15166
+ }
15167
+
14267
15168
  // packages/workit-core/src/core/task-engine.ts
15169
+ var workspaceVerification = (root) => {
15170
+ try {
15171
+ return resolveAutonomy(root).verification;
15172
+ } catch {
15173
+ return "self";
15174
+ }
15175
+ };
14268
15176
  var provenance = (context, kind = context.provenanceKind ?? "host_observed") => ({
14269
15177
  kind,
14270
15178
  host: context.caller.host,
@@ -14457,22 +15365,7 @@ var mapRef = (ref, ids) => {
14457
15365
  return ref;
14458
15366
  return { ...ref, id: ids.get(ref.id) ?? ref.id };
14459
15367
  };
14460
- var portableRefs = (refs) => refs.filter((ref) => ref.kind === "file" || ref.kind === "record");
14461
15368
  var portableRef = (ref) => ref && (ref.kind === "file" || ref.kind === "record") ? ref : null;
14462
- var portableFact = (fact) => {
14463
- const refs = portableRefs(fact.refs);
14464
- return fact.basis === "observed" && refs.length === 0 ? { ...fact, basis: "unknown", refs } : { ...fact, refs };
14465
- };
14466
- var portableSignal = (signal) => {
14467
- const refs = portableRefs(signal.refs);
14468
- return signal.basis === "observed" && refs.length === 0 ? {
14469
- ...signal,
14470
- value: "unknown",
14471
- basis: "unknown",
14472
- reason: "portable supporting reference was removed",
14473
- refs
14474
- } : { ...signal, refs };
14475
- };
14476
15369
  var importedProvenance = (context) => ({
14477
15370
  kind: "imported",
14478
15371
  host: context.caller.host,
@@ -14484,18 +15377,6 @@ var portableTask = (task) => {
14484
15377
  if (typeof stripped !== "object" || stripped === null || Array.isArray(stripped))
14485
15378
  throw new TypeError("portable task rewrite produced a non-record");
14486
15379
  const clone = structuredClone(stripped);
14487
- const demote = (facts, signals, consequences) => {
14488
- for (const fact of facts)
14489
- Object.assign(fact, portableFact(fact));
14490
- for (const name of Object.keys(signals)) {
14491
- const signal = signals[name];
14492
- Object.assign(signal, portableSignal(signal));
14493
- }
14494
- for (const consequence of consequences)
14495
- Object.assign(consequence.fact, portableFact(consequence.fact));
14496
- };
14497
- for (const entry of clone.assessments)
14498
- demote(entry.data.facts, entry.data.signals, entry.data.consequences);
14499
15380
  clone.candidates = [];
14500
15381
  return clone;
14501
15382
  };
@@ -14512,6 +15393,7 @@ var importedTask = (source, bundle, destinationWorkspaceId, context, timestamp)
14512
15393
  for (const entry of [
14513
15394
  source.intent,
14514
15395
  ...source.assessments,
15396
+ ...source.judgments ?? [],
14515
15397
  ...source.evidence,
14516
15398
  ...source.decisions,
14517
15399
  ...source.findings,
@@ -14535,9 +15417,16 @@ var importedTask = (source, bundle, destinationWorkspaceId, context, timestamp)
14535
15417
  id: fresh(entry.id),
14536
15418
  recordedAt: timestamp,
14537
15419
  provenance: imported,
14538
- data: rewriteRecordRefs(data, assessmentSchema, remap)
15420
+ data: rewriteRecordRefs(data, legacyAssessmentSchema, remap)
14539
15421
  };
14540
15422
  });
15423
+ const judgments = source.judgments?.map((entry) => ({
15424
+ ...entry,
15425
+ id: fresh(entry.id),
15426
+ recordedAt: timestamp,
15427
+ provenance: imported,
15428
+ data: rewriteRecordRefs(entry.data, judgmentSchema, remap)
15429
+ }));
14541
15430
  const evidence = source.evidence.map((entry) => ({
14542
15431
  ...entry,
14543
15432
  id: fresh(entry.id),
@@ -14605,6 +15494,7 @@ var importedTask = (source, bundle, destinationWorkspaceId, context, timestamp)
14605
15494
  status: "paused",
14606
15495
  closure: null,
14607
15496
  assessments,
15497
+ ...judgments ? { judgments } : {},
14608
15498
  evidence,
14609
15499
  decisions: decisions.map((entry) => {
14610
15500
  const data = rewriteRecordRefs(entry.data, decisionSchema, remap);
@@ -14674,7 +15564,8 @@ class WorkitCore {
14674
15564
  retryOmittedRevisions(request, run) {
14675
15565
  return retryFilledRevisions(filledRevisions(request), run);
14676
15566
  }
14677
- implicitTask(family, request) {
15567
+ implicitTask(family, raw) {
15568
+ const request = normalizeOperationInput(family, raw, this.context.caller.host);
14678
15569
  if (typeof request !== "object" || request === null || Array.isArray(request))
14679
15570
  return success(null, null, request);
14680
15571
  const value = request;
@@ -14928,7 +15819,11 @@ class WorkitCore {
14928
15819
  if ((this.context.workerId ?? null) !== null)
14929
15820
  return failure2("permission_denied", "helpers cannot change task requirements");
14930
15821
  this.fillRevisions(input, task.data);
14931
- const resolved = this.resolve(task.data, input.assessment);
15822
+ const judged = normalizeJudgment(input.judgment, latestJudgment(task.data));
15823
+ if (!judged.ok)
15824
+ return judged;
15825
+ const judgment = judged.data.judgment;
15826
+ const resolved = this.resolve(task.data, judgment);
14932
15827
  if (!resolved.ok)
14933
15828
  return resolved;
14934
15829
  if (input.action === "preview")
@@ -14939,16 +15834,17 @@ class WorkitCore {
14939
15834
  if (current.status === "closed")
14940
15835
  return failure2("invalid_transition", "closed task cannot be assessed");
14941
15836
  const nextChange = diffPolicy(current.policy, resolved.data, "policy reassessed", mutation.now);
14942
- const assessment = {
15837
+ const entry = {
14943
15838
  id: newId(),
14944
15839
  recordedAt: mutation.now,
14945
15840
  provenance: provenance(this.context),
14946
- data: input.assessment
15841
+ data: judgment
14947
15842
  };
14948
15843
  const next = {
14949
15844
  ...current,
14950
15845
  constraints: this.context.constraints,
14951
- assessments: [...current.assessments, assessment],
15846
+ judgments: [...current.judgments ?? [], entry],
15847
+ critical: [...new Set([...current.critical ?? [], "judgments"])],
14952
15848
  policy: resolved.data,
14953
15849
  policyChanges: nextChange ? [...current.policyChanges, nextChange] : current.policyChanges
14954
15850
  };
@@ -15150,9 +16046,14 @@ class WorkitCore {
15150
16046
  return workspace;
15151
16047
  if (!workspace.data)
15152
16048
  return failure2("not_found", "workspace not found");
15153
- if (input.binding.taskId !== task.data.id || input.binding.workspaceId !== workspace.data.id)
16049
+ const binding = {
16050
+ ...input.binding,
16051
+ taskId: input.binding.taskId ?? task.data.id,
16052
+ workspaceId: input.binding.workspaceId ?? workspace.data.id
16053
+ };
16054
+ if (binding.taskId !== task.data.id || binding.workspaceId !== workspace.data.id)
15154
16055
  return failure2("invalid_input", "decision task or workspace binding is invalid");
15155
- const content = verifyDecisionContentAtRoot(this.store.root, input.binding);
16056
+ const content = verifyDecisionContentAtRoot(this.store.root, binding);
15156
16057
  if (!content.ok)
15157
16058
  return content;
15158
16059
  const knownRequirements = new Set(task.data.policy?.requirements.map((item) => item.id) ?? []);
@@ -15160,7 +16061,7 @@ class WorkitCore {
15160
16061
  return failure2("invalid_input", "decision references an unknown requirement");
15161
16062
  const base = {
15162
16063
  purpose: input.purpose,
15163
- binding: input.binding,
16064
+ binding,
15164
16065
  response: input.response,
15165
16066
  requirementIds: input.requirementIds,
15166
16067
  revoked: null
@@ -15519,16 +16420,12 @@ class WorkitCore {
15519
16420
  now: trustedNow(this.context)
15520
16421
  });
15521
16422
  }
15522
- resolve(task, assessment) {
16423
+ resolve(task, judgment) {
15523
16424
  return resolvePolicy({
15524
16425
  intent: task.intent.data,
15525
- assessment,
16426
+ judgment,
15526
16427
  constraints: this.context.constraints,
15527
- prior: {
15528
- decisions: task.decisions.filter((entry) => entry.provenance.kind !== "imported").map((entry) => entry.data),
15529
- findings: task.findings.filter((entry) => entry.provenance.kind !== "imported").map((entry) => entry.data),
15530
- requirements: task.origin || task.policy === null ? [] : task.policy.requirements
15531
- }
16428
+ verification: workspaceVerification(this.context.root)
15532
16429
  });
15533
16430
  }
15534
16431
  summary(task) {
@@ -15854,9 +16751,9 @@ class WorkitCore {
15854
16751
  }
15855
16752
  }
15856
16753
  // packages/workit-core/src/core/repo-context.ts
15857
- import { spawnSync as spawnSync7 } from "node:child_process";
15858
- import { existsSync as existsSync5, readFileSync as readFileSync5, readdirSync as readdirSync4 } from "node:fs";
15859
- import path13 from "node:path";
16754
+ import { spawnSync as spawnSync8 } from "node:child_process";
16755
+ import { existsSync as existsSync6, readFileSync as readFileSync5, readdirSync as readdirSync4 } from "node:fs";
16756
+ import path16 from "node:path";
15860
16757
  var isSafeContextRange = (value) => value.length <= 4096 && (() => {
15861
16758
  try {
15862
16759
  gitRevisionParts(value);
@@ -15883,7 +16780,7 @@ var invalidRange = (cwd) => ({
15883
16780
  cwd
15884
16781
  });
15885
16782
  var runGit = (cwd, args) => {
15886
- const result = spawnSync7("git", args, { cwd, encoding: "utf8" });
16783
+ const result = spawnSync8("git", args, { cwd, encoding: "utf8" });
15887
16784
  return {
15888
16785
  stdout: (result.stdout ?? "").trimEnd(),
15889
16786
  stderr: (result.stderr ?? "").trimEnd(),
@@ -15894,11 +16791,11 @@ var repoRoot = (cwd) => {
15894
16791
  const r = runGit(cwd, ["rev-parse", "--show-toplevel"]);
15895
16792
  return r.exitCode === 0 && r.stdout ? r.stdout : cwd;
15896
16793
  };
15897
- var currentBranch = (cwd) => {
16794
+ var currentBranch2 = (cwd) => {
15898
16795
  const r = runGit(cwd, ["rev-parse", "--abbrev-ref", "HEAD"]);
15899
16796
  return r.exitCode === 0 && r.stdout ? r.stdout : "unknown";
15900
16797
  };
15901
- var defaultBase = (cwd) => {
16798
+ var defaultBase2 = (cwd) => {
15902
16799
  for (const ref of [
15903
16800
  "origin/main",
15904
16801
  "main",
@@ -15912,11 +16809,11 @@ var defaultBase = (cwd) => {
15912
16809
  }
15913
16810
  return "HEAD~1";
15914
16811
  };
15915
- var rangeArgOrDefault = (arg, cwd) => arg ? arg : `${defaultBase(cwd)}...HEAD`;
16812
+ var rangeArgOrDefault = (arg, cwd) => arg ? arg : `${defaultBase2(cwd)}...HEAD`;
15916
16813
  var isProtectedBranch = (branch) => ["main", "master", "develop", "prod", "production"].includes(branch);
15917
16814
  var isPrBranch = (branch) => branch.startsWith("feature/") || branch.startsWith("bugfix/");
15918
16815
  function resolvePrBranchContext(cwd) {
15919
- const branch = currentBranch(cwd);
16816
+ const branch = currentBranch2(cwd);
15920
16817
  if (isProtectedBranch(branch)) {
15921
16818
  return {
15922
16819
  ok: false,
@@ -15990,7 +16887,7 @@ var findPrTemplate = (cwd) => {
15990
16887
  ]) {
15991
16888
  const parts = rel.split("/");
15992
16889
  const base = parts[parts.length - 1];
15993
- const dir = parts.length > 1 ? path13.join(cwd, ...parts.slice(0, -1)) : cwd;
16890
+ const dir = parts.length > 1 ? path16.join(cwd, ...parts.slice(0, -1)) : cwd;
15994
16891
  const actual = probeCaseInsensitive(dir, base);
15995
16892
  if (actual)
15996
16893
  return [...parts.slice(0, -1), actual].join("/");
@@ -16021,8 +16918,8 @@ var diffStatForRange = (cwd, range) => {
16021
16918
  return runGit(cwd, ["diff", "--stat", "--"]).stdout;
16022
16919
  };
16023
16920
  var packageScripts = (cwd, keys) => {
16024
- const pkgPath = path13.join(cwd, "package.json");
16025
- if (!existsSync5(pkgPath))
16921
+ const pkgPath = path16.join(cwd, "package.json");
16922
+ if (!existsSync6(pkgPath))
16026
16923
  return [];
16027
16924
  try {
16028
16925
  const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
@@ -16047,9 +16944,9 @@ var documentationFiles = (cwd) => {
16047
16944
  if (entry.isDirectory()) {
16048
16945
  if (excluded.has(entry.name))
16049
16946
  continue;
16050
- walk(path13.join(dir, entry.name), depth + 1);
16947
+ walk(path16.join(dir, entry.name), depth + 1);
16051
16948
  } else if (entry.isFile() && (entry.name === "README.md" || entry.name.endsWith(".md"))) {
16052
- matches.push("./" + path13.relative(cwd, path13.join(dir, entry.name)).replaceAll("\\", "/"));
16949
+ matches.push("./" + path16.relative(cwd, path16.join(dir, entry.name)).replaceAll("\\", "/"));
16053
16950
  }
16054
16951
  }
16055
16952
  };
@@ -16057,7 +16954,7 @@ var documentationFiles = (cwd) => {
16057
16954
  return matches.toSorted().slice(0, 200);
16058
16955
  };
16059
16956
  var readTrimmed2 = (file, maxLines) => {
16060
- if (!existsSync5(file))
16957
+ if (!existsSync6(file))
16061
16958
  return "";
16062
16959
  const lines = readFileSync5(file, "utf8").split(`
16063
16960
  `);
@@ -16065,7 +16962,7 @@ var readTrimmed2 = (file, maxLines) => {
16065
16962
  `);
16066
16963
  };
16067
16964
  function prReadyContext(root, range) {
16068
- const cwd = path13.resolve(repoRoot(root));
16965
+ const cwd = path16.resolve(repoRoot(root));
16069
16966
  if (range !== undefined && !isResolvableContextRange(cwd, range))
16070
16967
  return invalidRange(cwd);
16071
16968
  let stdout = "";
@@ -16081,7 +16978,7 @@ function prReadyContext(root, range) {
16081
16978
  stdout += printSection("Repository");
16082
16979
  stdout += `root: ${cwd}
16083
16980
  `;
16084
- stdout += `branch: ${currentBranch(cwd)}
16981
+ stdout += `branch: ${currentBranch2(cwd)}
16085
16982
  `;
16086
16983
  stdout += `range: ${range}
16087
16984
  `;
@@ -16123,7 +17020,7 @@ ${status.stderr}` : "");
16123
17020
  stdout += `template_path: ${template}
16124
17021
 
16125
17022
  `;
16126
- stdout += readTrimmed2(path13.join(cwd, template), 220);
17023
+ stdout += readTrimmed2(path16.join(cwd, template), 220);
16127
17024
  } else {
16128
17025
  stdout += `template_path: none
16129
17026
 
@@ -16133,7 +17030,7 @@ ${status.stderr}` : "");
16133
17030
  stdout += `
16134
17031
  `;
16135
17032
  stdout += printSection("Recent Validation Signals");
16136
- if (existsSync5(path13.join(cwd, "package.json"))) {
17033
+ if (existsSync6(path16.join(cwd, "package.json"))) {
16137
17034
  stdout += `package.json detected. Common scripts:
16138
17035
  `;
16139
17036
  const found = packageScripts(cwd, ["lint", "format:check", "test", "build"]);
@@ -16142,7 +17039,7 @@ ${status.stderr}` : "");
16142
17039
  `) + `
16143
17040
  `;
16144
17041
  }
16145
- if (existsSync5(path13.join(cwd, "Cargo.toml")) || existsSync5(path13.join(cwd, "src-tauri/Cargo.toml"))) {
17042
+ if (existsSync6(path16.join(cwd, "Cargo.toml")) || existsSync6(path16.join(cwd, "src-tauri/Cargo.toml"))) {
16146
17043
  stdout += `Rust project detected.
16147
17044
  `;
16148
17045
  }
@@ -16175,7 +17072,7 @@ var CHANGELOG_RULES = `- Use an [Unreleased] section.
16175
17072
  - Apply with the host-native editor, preserving the repository changelog conventions.
16176
17073
  - If Unreleased already has duplicate category headings, normalize_only first.`;
16177
17074
  function changelogContext(root, range) {
16178
- const cwd = path13.resolve(repoRoot(root));
17075
+ const cwd = path16.resolve(repoRoot(root));
16179
17076
  if (range !== undefined && !isResolvableContextRange(cwd, range))
16180
17077
  return invalidRange(cwd);
16181
17078
  const resolvedRange = rangeArgOrDefault(range, cwd);
@@ -16185,7 +17082,7 @@ function changelogContext(root, range) {
16185
17082
  stdout += printSection("Repository");
16186
17083
  stdout += `root: ${cwd}
16187
17084
  `;
16188
- stdout += `branch: ${currentBranch(cwd)}
17085
+ stdout += `branch: ${currentBranch2(cwd)}
16189
17086
  `;
16190
17087
  stdout += `range: ${resolvedRange}
16191
17088
  `;
@@ -16193,8 +17090,8 @@ function changelogContext(root, range) {
16193
17090
  stdout += CHANGELOG_RULES + `
16194
17091
  `;
16195
17092
  stdout += printSection("Existing CHANGELOG.md");
16196
- const changelogPath = path13.join(cwd, "CHANGELOG.md");
16197
- if (existsSync5(changelogPath)) {
17093
+ const changelogPath = path16.join(cwd, "CHANGELOG.md");
17094
+ if (existsSync6(changelogPath)) {
16198
17095
  stdout += readTrimmed2(changelogPath, 260) + `
16199
17096
  `;
16200
17097
  } else {
@@ -16213,7 +17110,7 @@ function changelogContext(root, range) {
16213
17110
  return { stdout, stderr: "", exitCode: 0, cwd };
16214
17111
  }
16215
17112
  function docsRefreshContext(root, range) {
16216
- const cwd = path13.resolve(repoRoot(root));
17113
+ const cwd = path16.resolve(repoRoot(root));
16217
17114
  if (range !== undefined && !isResolvableContextRange(cwd, range))
16218
17115
  return invalidRange(cwd);
16219
17116
  const resolvedRange = rangeArgOrDefault(range, cwd);
@@ -16223,7 +17120,7 @@ function docsRefreshContext(root, range) {
16223
17120
  stdout += printSection("Repository");
16224
17121
  stdout += `root: ${cwd}
16225
17122
  `;
16226
- stdout += `branch: ${currentBranch(cwd)}
17123
+ stdout += `branch: ${currentBranch2(cwd)}
16227
17124
  `;
16228
17125
  stdout += `range: ${resolvedRange}
16229
17126
  `;
@@ -16235,8 +17132,8 @@ function docsRefreshContext(root, range) {
16235
17132
  `) + `
16236
17133
  `;
16237
17134
  stdout += printSection("README Preview");
16238
- const readme = path13.join(cwd, "README.md");
16239
- if (existsSync5(readme)) {
17135
+ const readme = path16.join(cwd, "README.md");
17136
+ if (existsSync6(readme)) {
16240
17137
  stdout += readTrimmed2(readme, 220) + `
16241
17138
  `;
16242
17139
  } else {
@@ -16244,8 +17141,8 @@ function docsRefreshContext(root, range) {
16244
17141
  `;
16245
17142
  }
16246
17143
  stdout += printSection("Package Scripts");
16247
- const pkgPath = path13.join(cwd, "package.json");
16248
- if (existsSync5(pkgPath)) {
17144
+ const pkgPath = path16.join(cwd, "package.json");
17145
+ if (existsSync6(pkgPath)) {
16249
17146
  try {
16250
17147
  const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
16251
17148
  stdout += JSON.stringify({ name: pkg.name, version: pkg.version, scripts: pkg.scripts }, null, 2) + `
@@ -16258,7 +17155,7 @@ function docsRefreshContext(root, range) {
16258
17155
  return { stdout, stderr: "", exitCode: 0, cwd };
16259
17156
  }
16260
17157
  function releaseNotesContext(root, rangeOrTag) {
16261
- const cwd = path13.resolve(repoRoot(root));
17158
+ const cwd = path16.resolve(repoRoot(root));
16262
17159
  if (!rangeOrTag) {
16263
17160
  return { stdout: "", stderr: `ERROR: release tag or range required
16264
17161
  `, exitCode: 1, cwd };
@@ -16272,7 +17169,7 @@ function releaseNotesContext(root, rangeOrTag) {
16272
17169
  stdout += printSection("Repository");
16273
17170
  stdout += `root: ${cwd}
16274
17171
  `;
16275
- stdout += `branch: ${currentBranch(cwd)}
17172
+ stdout += `branch: ${currentBranch2(cwd)}
16276
17173
  `;
16277
17174
  stdout += `requested: ${rangeOrTag}
16278
17175
  `;
@@ -16296,7 +17193,7 @@ function releaseNotesContext(root, rangeOrTag) {
16296
17193
  `;
16297
17194
  stdout += printSection("Existing Release Files");
16298
17195
  for (const rel of ["CHANGELOG.md", "RELEASE_NOTES.md", ".github/releases.md"]) {
16299
- if (existsSync5(path13.join(cwd, rel)))
17196
+ if (existsSync6(path16.join(cwd, rel)))
16300
17197
  stdout += rel + `
16301
17198
  `;
16302
17199
  }
@@ -16324,12 +17221,12 @@ function releaseNotesContext(root, rangeOrTag) {
16324
17221
  }
16325
17222
 
16326
17223
  // packages/workit-core/src/core/tracker-issues.ts
16327
- import { spawnSync as spawnSync9 } from "node:child_process";
17224
+ import { spawnSync as spawnSync10 } from "node:child_process";
16328
17225
 
16329
17226
  // packages/workit-core/src/core/pr-create.ts
16330
- import fs12 from "node:fs";
16331
- import path14 from "node:path";
16332
- import { spawnSync as spawnSync8 } from "node:child_process";
17227
+ import fs13 from "node:fs";
17228
+ import path17 from "node:path";
17229
+ import { spawnSync as spawnSync9 } from "node:child_process";
16333
17230
  // packages/workit-core/src/forge/pr-body.ts
16334
17231
  function parseGhIssue(value) {
16335
17232
  const m = /issues\/(\d+)/.exec(value);
@@ -16339,13 +17236,13 @@ function parseGhIssue(value) {
16339
17236
  // packages/workit-core/src/core/pr-create.ts
16340
17237
  function whichOnPath(tool) {
16341
17238
  const names = process.platform === "win32" ? [tool, `${tool}.exe`, `${tool}.cmd`] : [tool];
16342
- for (const dir of (process.env.PATH ?? "").split(path14.delimiter)) {
17239
+ for (const dir of (process.env.PATH ?? "").split(path17.delimiter)) {
16343
17240
  if (!dir)
16344
17241
  continue;
16345
17242
  for (const name of names) {
16346
- const candidate = path14.join(dir, name);
17243
+ const candidate = path17.join(dir, name);
16347
17244
  try {
16348
- fs12.accessSync(candidate, fs12.constants.X_OK);
17245
+ fs13.accessSync(candidate, fs13.constants.X_OK);
16349
17246
  return candidate;
16350
17247
  } catch {}
16351
17248
  }
@@ -16354,7 +17251,7 @@ function whichOnPath(tool) {
16354
17251
  }
16355
17252
  var hostingCliAvailable = (provider) => whichOnPath(provider === "gitlab" ? "glab" : "gh") !== null;
16356
17253
  var pushRemote = (cwd) => {
16357
- const result = spawnSync8("git", ["remote", "get-url", "--push", "--all", "origin"], {
17254
+ const result = spawnSync9("git", ["remote", "get-url", "--push", "--all", "origin"], {
16358
17255
  cwd,
16359
17256
  encoding: "utf8"
16360
17257
  });
@@ -16385,7 +17282,7 @@ var defaultCli = (bin, available) => {
16385
17282
  if (!available)
16386
17283
  return null;
16387
17284
  return (args, root) => {
16388
- const out = spawnSync9(bin, args, {
17285
+ const out = spawnSync10(bin, args, {
16389
17286
  cwd: root,
16390
17287
  encoding: "utf8",
16391
17288
  env: { ...process.env, PATH: process.env.PATH ?? "" }
@@ -16463,8 +17360,8 @@ var fetchGitLabIssueBody = async (ref, root, deps = {}) => {
16463
17360
  };
16464
17361
 
16465
17362
  // packages/workit-core/src/core/youtrack.ts
16466
- import fs13 from "node:fs";
16467
- import path15 from "node:path";
17363
+ import fs14 from "node:fs";
17364
+ import path18 from "node:path";
16468
17365
 
16469
17366
  // packages/workit-core/src/core/templates.ts
16470
17367
  var repoRoot2 = assetRoot();
@@ -16477,22 +17374,22 @@ var resolveWorkspaceRoot = (explicit) => explicit || process.cwd();
16477
17374
  // packages/workit-core/src/core/youtrack.ts
16478
17375
  var ISSUE_RE = /^[A-Z]+-\d+$/;
16479
17376
  var TOKEN_PLACEHOLDER = "YOUR_TOKEN_HERE";
16480
- var youTrackConfigPath = () => process.env.WORKFLOW_YOUTRACK_CONFIG ?? path15.join(configDir(), "youtrack.json");
16481
- var youTrackReadOnlyConfigPath = () => process.env.WORKFLOW_YOUTRACK_CONFIG ?? path15.join(resolveConfigDir(), "youtrack.json");
17377
+ var youTrackConfigPath = () => process.env.WORKFLOW_YOUTRACK_CONFIG ?? path18.join(configDir(), "youtrack.json");
17378
+ var youTrackReadOnlyConfigPath = () => process.env.WORKFLOW_YOUTRACK_CONFIG ?? path18.join(resolveConfigDir(), "youtrack.json");
16482
17379
  var youTrackTokenModeOk = (p) => {
16483
17380
  if (process.platform === "win32")
16484
17381
  return true;
16485
- const mode = fs13.statSync(p).mode & 511;
17382
+ const mode = fs14.statSync(p).mode & 511;
16486
17383
  return mode === 384;
16487
17384
  };
16488
17385
  function readYouTrackConfig(required) {
16489
17386
  const cfgPath = youTrackConfigPath();
16490
- if (!fs13.existsSync(cfgPath)) {
17387
+ if (!fs14.existsSync(cfgPath)) {
16491
17388
  return required ? { error: "ERROR: missing youtrack.json", path: cfgPath } : { config: {}, path: cfgPath };
16492
17389
  }
16493
17390
  let parsed;
16494
17391
  try {
16495
- parsed = JSON.parse(fs13.readFileSync(cfgPath, "utf8"));
17392
+ parsed = JSON.parse(fs14.readFileSync(cfgPath, "utf8"));
16496
17393
  } catch {
16497
17394
  return { error: `${cfgPath} is not valid JSON`, path: cfgPath };
16498
17395
  }
@@ -16503,11 +17400,11 @@ function readYouTrackConfig(required) {
16503
17400
  }
16504
17401
  var readYouTrackContextConfig = () => {
16505
17402
  const cfgPath = youTrackReadOnlyConfigPath();
16506
- if (!fs13.existsSync(cfgPath))
17403
+ if (!fs14.existsSync(cfgPath))
16507
17404
  return { data: {} };
16508
17405
  let parsed;
16509
17406
  try {
16510
- parsed = JSON.parse(fs13.readFileSync(cfgPath, "utf8"));
17407
+ parsed = JSON.parse(fs14.readFileSync(cfgPath, "utf8"));
16511
17408
  } catch {
16512
17409
  return { error: "YouTrack configuration is unavailable" };
16513
17410
  }
@@ -16546,12 +17443,12 @@ var youTrackToken = () => {
16546
17443
  if ("error" in loaded)
16547
17444
  return loaded;
16548
17445
  const tokenFile = String(loaded.config.tokenFile ?? "");
16549
- const tokenPath = tokenFile ? path15.isAbsolute(tokenFile) ? path15.resolve(tokenFile) : path15.resolve(process.cwd(), tokenFile) : "";
16550
- if (!tokenPath || !fs13.existsSync(tokenPath))
17446
+ const tokenPath = tokenFile ? path18.isAbsolute(tokenFile) ? path18.resolve(tokenFile) : path18.resolve(process.cwd(), tokenFile) : "";
17447
+ if (!tokenPath || !fs14.existsSync(tokenPath))
16551
17448
  return { error: "missing youtrack.token" };
16552
17449
  if (!youTrackTokenModeOk(tokenPath))
16553
17450
  return { error: "youtrack.token mode must be 0600" };
16554
- const token = fs13.readFileSync(tokenPath, "utf8").trim();
17451
+ const token = fs14.readFileSync(tokenPath, "utf8").trim();
16555
17452
  if (!token)
16556
17453
  return { error: "empty token file" };
16557
17454
  if (token === TOKEN_PLACEHOLDER || token.startsWith(TOKEN_PLACEHOLDER)) {
@@ -16616,9 +17513,9 @@ function resolveYouTrackFromPaths(spec_path, plan_path, workspace_root) {
16616
17513
  } catch {
16617
17514
  continue;
16618
17515
  }
16619
- if (!fs13.existsSync(full))
17516
+ if (!fs14.existsSync(full))
16620
17517
  continue;
16621
- const text = fs13.readFileSync(full, "utf8");
17518
+ const text = fs14.readFileSync(full, "utf8");
16622
17519
  const m = text.match(/^\*\*YouTrack:\*\*\s*`?([A-Z]+-\d+)`?/m);
16623
17520
  if (m)
16624
17521
  return m[1];
@@ -16903,22 +17800,22 @@ function resolveGitRevision(root, value) {
16903
17800
  }
16904
17801
  function resolveInside(root, candidate) {
16905
17802
  const base = realpathSync5(root);
16906
- const target = path16.resolve(base, candidate);
16907
- if (target !== base && !target.startsWith(base + path16.sep)) {
17803
+ const target = path19.resolve(base, candidate);
17804
+ if (target !== base && !target.startsWith(base + path19.sep)) {
16908
17805
  throw new Error("path must stay inside repository root");
16909
17806
  }
16910
17807
  let ancestor = target;
16911
- while (!existsSync6(ancestor))
16912
- ancestor = path16.dirname(ancestor);
17808
+ while (!existsSync7(ancestor))
17809
+ ancestor = path19.dirname(ancestor);
16913
17810
  const canonicalAncestor = realpathSync5(ancestor);
16914
- if (canonicalAncestor !== base && !canonicalAncestor.startsWith(base + path16.sep)) {
17811
+ if (canonicalAncestor !== base && !canonicalAncestor.startsWith(base + path19.sep)) {
16915
17812
  throw new Error("path must stay inside repository root");
16916
17813
  }
16917
17814
  return target;
16918
17815
  }
16919
17816
  function run2(root, executable, args, env = {}) {
16920
17817
  const cwd = realpathSync5(root);
16921
- const result = spawnSync10(executable, args, {
17818
+ const result = spawnSync11(executable, args, {
16922
17819
  cwd,
16923
17820
  encoding: "utf8",
16924
17821
  env: { ...process.env, ...env }
@@ -16955,7 +17852,7 @@ var capabilitiesFor = (descriptor, observed = {}) => descriptor.capabilities.map
16955
17852
  };
16956
17853
  });
16957
17854
  // packages/workit-core/src/hooks/context.ts
16958
- import path17 from "node:path";
17855
+ import path20 from "node:path";
16959
17856
  var MIGRATION_PENDING_NOTE = "workit migration pending — run `workit task status`";
16960
17857
  var migrationPending = (result) => !result.ok && result.code === "needs_input" && (result.error ?? "").startsWith(MIGRATION_PENDING);
16961
17858
  var boundTo = (entry, session) => entry.sessions.some((item) => item.host === session.host && item.handle === session.handle);
@@ -17034,7 +17931,7 @@ function entryCompactContext(store, entry, session, context) {
17034
17931
  return null;
17035
17932
  const freshness = { mode: "signal", current: () => recentSignal(store.root) };
17036
17933
  const observed = task.data.evidence.some(isObservedCheck);
17037
- const files = task.data.decisions.flatMap((decision) => decision.data.binding.contentRefs.flatMap((ref) => ref.kind === "file" ? [path17.resolve(store.root, ref.path)] : []));
17934
+ const files = task.data.decisions.flatMap((decision) => decision.data.binding.contentRefs.flatMap((ref) => ref.kind === "file" ? [path20.resolve(store.root, ref.path)] : []));
17038
17935
  const observedFiles = filesKey(files);
17039
17936
  const compact = new WorkitCore(store, context).compactContext(entry.id, freshness);
17040
17937
  if (!compact.ok)
@@ -17051,17 +17948,424 @@ function entryCompactContext(store, entry, session, context) {
17051
17948
  cache.delete(cache.keys().next().value);
17052
17949
  return compact.data;
17053
17950
  }
17951
+ var utcNow = () => new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
17952
+ var hookOperationContext = (input, descriptor) => ({
17953
+ root: input.cwd,
17954
+ caller: { host: input.host, actor: input.session.id },
17955
+ callerAttested: false,
17956
+ capabilities: capabilitiesFor(descriptor, { "session.start": true }),
17957
+ constraints: [],
17958
+ now: utcNow
17959
+ });
17960
+ var sessionContextText = (input, descriptor, options) => {
17961
+ const session = { host: input.host, handle: input.session.id };
17962
+ let compact = "";
17963
+ let offer = null;
17964
+ try {
17965
+ const store = new TaskStore(input.cwd);
17966
+ const listed = store.listTaskIndex();
17967
+ if (migrationPending(listed))
17968
+ return `<workit-contract>
17969
+ ${invariantBootstrap()}
17970
+ ${MIGRATION_PENDING_NOTE}${options.addendum ? `
17971
+ ${options.addendum}` : ""}
17972
+ </workit-contract>`;
17973
+ if (!listed.ok)
17974
+ throw new Error(listed.error);
17975
+ const entry = currentTaskEntry(listed.data, session, descriptor.context.task) ?? implicitTaskEntry(store, listed.data, session);
17976
+ const text = entry ? entryCompactContext(store, entry, session, hookOperationContext(input, descriptor)) : null;
17977
+ if (text)
17978
+ compact = `
17979
+ <workit-task-context>${text}</workit-task-context>`;
17980
+ if (options.offer)
17981
+ offer = unfinishedTaskOffer(listed.data, session, entry?.id ?? null);
17982
+ } catch {
17983
+ compact = `
17984
+ [workit diagnostic: task state unavailable]`;
17985
+ }
17986
+ return `<workit-contract>
17987
+ ${invariantBootstrap()}${compact}${offer ? `
17988
+ ${offer}` : ""}${options.addendum ? `
17989
+ ${options.addendum}` : ""}
17990
+ </workit-contract>`;
17991
+ };
17992
+ var turnContextText = (input, descriptor) => {
17993
+ try {
17994
+ const text = sessionCompactContext(new TaskStore(input.cwd), { host: input.host, handle: input.session.id }, hookOperationContext(input, descriptor), descriptor.context.task);
17995
+ if (text === MIGRATION_PENDING_NOTE)
17996
+ return text;
17997
+ return text ? `<workit-task-context>${text}</workit-task-context>` : null;
17998
+ } catch {
17999
+ return null;
18000
+ }
18001
+ };
17054
18002
  // packages/workit-core/src/hooks/policy.ts
17055
18003
  var shellPolicy = (cwd, command) => {
17056
18004
  const policy = shellBranchPolicyViolation(cwd, command);
17057
18005
  return policy && !policy.ok ? { kind: "deny", reason: `branch_policy_denied: ${policy.error}`, unblock: null } : { kind: "none" };
17058
18006
  };
18007
+ // packages/workit-core/src/hooks/write-gate.ts
18008
+ import { realpathSync as realpathSync6 } from "node:fs";
18009
+ import path21 from "node:path";
18010
+ var NONE2 = { kind: "none" };
18011
+ var CASE_INSENSITIVE = process.platform === "darwin" || process.platform === "win32";
18012
+ var canonical2 = (file) => {
18013
+ let rest = "";
18014
+ for (let dir = path21.resolve(file);; dir = path21.dirname(dir)) {
18015
+ try {
18016
+ return path21.join(realpathSync6(dir), rest);
18017
+ } catch {
18018
+ if (path21.dirname(dir) === dir)
18019
+ return path21.resolve(file);
18020
+ rest = rest ? path21.join(path21.basename(dir), rest) : path21.basename(dir);
18021
+ }
18022
+ }
18023
+ };
18024
+ var exempt = (cwd, root, file, plans) => {
18025
+ const fold = (value) => CASE_INSENSITIVE ? value.toLowerCase() : value;
18026
+ const relative = path21.relative(fold(canonical2(root)), fold(canonical2(path21.resolve(cwd, file))));
18027
+ if (relative.startsWith("..") || path21.isAbsolute(relative))
18028
+ return true;
18029
+ const posix = relative.split(path21.sep).join("/");
18030
+ const segments = posix.split("/");
18031
+ return /\.mdx?$/i.test(posix) || segments[0] === "docs" || segments.includes("plans") || [...plans].some((plan) => fold(plan) === posix);
18032
+ };
18033
+ var segmentsOf = (command) => {
18034
+ const segments = [];
18035
+ let current = { words: [], redirects: [] };
18036
+ let word = "";
18037
+ let inWord = false;
18038
+ let pending = null;
18039
+ const heredocs = [];
18040
+ const flushWord = () => {
18041
+ if (!inWord)
18042
+ return;
18043
+ if (pending === "redirect") {
18044
+ if (!word.startsWith("&"))
18045
+ current.redirects.push(word);
18046
+ } else if (pending === "heredoc")
18047
+ heredocs.push(word.replace(/^-/, ""));
18048
+ else if (pending !== "input")
18049
+ current.words.push(word);
18050
+ pending = null;
18051
+ word = "";
18052
+ inWord = false;
18053
+ };
18054
+ const flushSegment = () => {
18055
+ flushWord();
18056
+ if (current.words.length || current.redirects.length)
18057
+ segments.push(current);
18058
+ current = { words: [], redirects: [] };
18059
+ };
18060
+ for (let index = 0;index < command.length; index++) {
18061
+ const char = command[index];
18062
+ if (char === "'" || char === '"') {
18063
+ const end = command.indexOf(char, index + 1);
18064
+ word += command.slice(index + 1, end < 0 ? undefined : end);
18065
+ inWord = true;
18066
+ index = end < 0 ? command.length : end;
18067
+ } else if (char === "\\" && index + 1 < command.length) {
18068
+ word += command[++index];
18069
+ inWord = true;
18070
+ } else if (char === `
18071
+ `) {
18072
+ flushSegment();
18073
+ while (heredocs.length) {
18074
+ const delimiter = heredocs.shift();
18075
+ const lines = command.slice(index + 1).split(`
18076
+ `);
18077
+ const at = lines.findIndex((line) => line.trim() === delimiter);
18078
+ const skipped = (at < 0 ? lines : lines.slice(0, at + 1)).join(`
18079
+ `).length + 1;
18080
+ index += skipped;
18081
+ }
18082
+ } else if (/\s/.test(char))
18083
+ flushWord();
18084
+ else if (";&|()".includes(char)) {
18085
+ if (char === "&" && command[index + 1] === ">")
18086
+ continue;
18087
+ flushSegment();
18088
+ } else if (char === ">") {
18089
+ if (inWord && /^\d$/.test(word)) {
18090
+ word = "";
18091
+ inWord = false;
18092
+ } else
18093
+ flushWord();
18094
+ if (command[index + 1] === ">")
18095
+ index++;
18096
+ if (command[index + 1] === "&") {
18097
+ index++;
18098
+ word = "&";
18099
+ inWord = true;
18100
+ }
18101
+ pending = "redirect";
18102
+ } else if (char === "<") {
18103
+ flushWord();
18104
+ if (command[index + 1] === "<") {
18105
+ index++;
18106
+ if (command[index + 1] === "<")
18107
+ index++;
18108
+ pending = "heredoc";
18109
+ } else
18110
+ pending = "input";
18111
+ } else {
18112
+ word += char;
18113
+ inWord = true;
18114
+ }
18115
+ }
18116
+ flushSegment();
18117
+ return segments;
18118
+ };
18119
+ var PREFIXES = new Set(["sudo", "command", "env", "nohup", "time", "exec", "xargs"]);
18120
+ var operands = (words) => words.filter((item) => !item.startsWith("-"));
18121
+ var editsOf = (input) => {
18122
+ let words = input;
18123
+ while (words.length && (PREFIXES.has(words[0]) || /^\w+=/.test(words[0])))
18124
+ words = words.slice(1);
18125
+ const [verb, ...args] = words;
18126
+ const name = (verb?.split(/[\\/]/).at(-1) ?? "").toLowerCase();
18127
+ if (!name || name === "workit" || name === "npx" && args.some((item) => /workit/.test(item)))
18128
+ return [];
18129
+ switch (name) {
18130
+ case "touch":
18131
+ case "mkdir":
18132
+ case "rm":
18133
+ case "rmdir":
18134
+ case "tee":
18135
+ case "mv":
18136
+ case "truncate":
18137
+ return operands(args);
18138
+ case "cp":
18139
+ case "ln":
18140
+ case "install": {
18141
+ const target = args.indexOf("-t");
18142
+ if (target >= 0)
18143
+ return args[target + 1] ? [args[target + 1]] : null;
18144
+ const last = operands(args).at(-1);
18145
+ return last ? [last] : null;
18146
+ }
18147
+ case "dd":
18148
+ return args.filter((item) => item.startsWith("of=")).map((item) => item.slice(3));
18149
+ case "sed":
18150
+ case "perl": {
18151
+ if (!args.some((item) => /^-[a-zA-Z]*i|^--in-place/.test(item)))
18152
+ return [];
18153
+ const scripted = args.some((item) => item === "-e" || item === "-f");
18154
+ const files = [];
18155
+ for (let index = 0;index < args.length; index++) {
18156
+ if (args[index] === "-e" || args[index] === "-f")
18157
+ index++;
18158
+ else if (!args[index].startsWith("-"))
18159
+ files.push(args[index]);
18160
+ }
18161
+ return scripted ? files : files.slice(1);
18162
+ }
18163
+ case "patch":
18164
+ return null;
18165
+ case "set-content":
18166
+ case "add-content":
18167
+ case "out-file":
18168
+ case "new-item":
18169
+ case "ni":
18170
+ case "remove-item":
18171
+ case "del":
18172
+ case "copy-item":
18173
+ case "move-item": {
18174
+ const flag = args.findIndex((item) => /^-(?:path|filepath|literalpath|destination)$/i.test(item));
18175
+ if (flag >= 0)
18176
+ return args[flag + 1] ? [args[flag + 1]] : null;
18177
+ const files = operands(args);
18178
+ if (name === "move-item")
18179
+ return files.length ? files : null;
18180
+ const file = name === "copy-item" ? files.at(-1) : files[0];
18181
+ return file ? [file] : null;
18182
+ }
18183
+ case "git": {
18184
+ const sub = operands(args)[0];
18185
+ if (sub === "apply")
18186
+ return null;
18187
+ if (sub === "restore" || sub === "mv" || sub === "rm")
18188
+ return operands(args).slice(1);
18189
+ if (sub === "checkout" && args.includes("--"))
18190
+ return args.slice(args.indexOf("--") + 1);
18191
+ return [];
18192
+ }
18193
+ default:
18194
+ return [];
18195
+ }
18196
+ };
18197
+ var shellWrites = (command) => {
18198
+ const targets = [];
18199
+ let unknown = false;
18200
+ for (const segment of segmentsOf(command)) {
18201
+ targets.push(...segment.redirects.filter((item) => !/^\/dev\/(?:null|stdout|stderr|tty)$/.test(item)));
18202
+ const edits = editsOf(segment.words);
18203
+ if (edits === null)
18204
+ unknown = true;
18205
+ else
18206
+ targets.push(...edits);
18207
+ }
18208
+ if (!unknown && targets.length === 0)
18209
+ return { writes: false, targets: null };
18210
+ return { writes: true, targets: unknown ? null : targets };
18211
+ };
18212
+ var cache2 = new Map;
18213
+ var planKeyOf = (root, plans) => [...plans].map((file) => fileSignature(path21.resolve(root, file)) ?? "-").join("\x00");
18214
+ var gateFor = (cwd) => {
18215
+ const store = new TaskStore(cwd);
18216
+ const listed = store.listTaskIndex();
18217
+ if (!listed.ok)
18218
+ return null;
18219
+ const key = store.currentKey();
18220
+ if (!key.ok)
18221
+ return null;
18222
+ const entry = listed.data.filter((item) => item.key === key.data.key && item.status !== "closed").toSorted((left, right) => right.updatedAt.localeCompare(left.updatedAt))[0];
18223
+ if (!entry)
18224
+ return { blockers: [], root: store.root, plans: new Set };
18225
+ const ledger = ledgerPath(store.root);
18226
+ const cacheKey = `${entry.id}\x00${entry.revision}\x00${ledger.ok && fileSignature(ledger.value) || "-"}`;
18227
+ const hit = cache2.get(store.root);
18228
+ if (hit && hit.key === cacheKey && hit.planKey === planKeyOf(store.root, hit.plans))
18229
+ return hit;
18230
+ const task = store.readTask(entry.id);
18231
+ if (!task.ok)
18232
+ return null;
18233
+ const workspace = store.readWorkspace();
18234
+ const plans = new Set((latestJudgment(task.data)?.refs ?? []).flatMap((ref) => ref.kind === "file" ? [ref.path] : []));
18235
+ const blockers = writeBlockers(task.data, store.root).filter((blocker) => {
18236
+ const requirement = task.data.policy?.requirements.find((item) => item.ruleId === blocker.ruleId && item.before === "write");
18237
+ return !(requirement?.acceptanceAllowed && workspace.ok && workspace.data && applicableDecision(task.data, workspace.data, requirement, store.root).length > 0);
18238
+ });
18239
+ const gate = {
18240
+ blockers,
18241
+ root: store.root,
18242
+ plans,
18243
+ key: cacheKey,
18244
+ planKey: planKeyOf(store.root, plans)
18245
+ };
18246
+ cache2.set(store.root, gate);
18247
+ if (cache2.size > 32)
18248
+ cache2.delete(cache2.keys().next().value);
18249
+ return gate;
18250
+ };
18251
+ function writeGate(cwd, paths) {
18252
+ try {
18253
+ const gate = gateFor(cwd);
18254
+ if (!gate || gate.blockers.length === 0)
18255
+ return NONE2;
18256
+ if (paths && paths.every((file) => exempt(cwd, gate.root, file, gate.plans)))
18257
+ return NONE2;
18258
+ const reasons = gate.blockers.map((item) => `${item.ruleId}: ${item.reason}`).join("; ");
18259
+ return {
18260
+ kind: "deny",
18261
+ reason: `workit before-write gate: ${reasons}. Markdown, docs/, plans/ and files outside the checkout stay writable.`,
18262
+ unblock: gate.blockers[0].reason
18263
+ };
18264
+ } catch {
18265
+ return NONE2;
18266
+ }
18267
+ }
18268
+ // packages/workit-core/src/hooks/hosts/fields.ts
18269
+ var isRecord3 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
18270
+ var nonEmpty2 = (value) => typeof value === "string" && value.trim() !== "";
18271
+ var SHELLS = new Set(["sh", "bash", "zsh", "dash", "pwsh", "powershell", "cmd"]);
18272
+ var WRITE_TOOLS = new Set([
18273
+ "write",
18274
+ "edit",
18275
+ "multiedit",
18276
+ "notebookedit",
18277
+ "applypatch",
18278
+ "patch",
18279
+ "delete",
18280
+ "remove",
18281
+ "rename",
18282
+ "mkdir",
18283
+ "mv",
18284
+ "cp",
18285
+ "touch",
18286
+ "strreplace",
18287
+ "searchreplace",
18288
+ "editfile",
18289
+ "writefile"
18290
+ ]);
18291
+ var PATH_KEYS = ["file_path", "filePath", "notebook_path", "path", "target_file", "file"];
18292
+ var writePaths = (input) => {
18293
+ if (!isRecord3(input))
18294
+ return [];
18295
+ const paths = PATH_KEYS.map((key) => input[key]).filter(nonEmpty2);
18296
+ for (const value of Object.values(input))
18297
+ if (typeof value === "string")
18298
+ for (const match of value.matchAll(/^\*\*\* (?:Add|Update|Delete) File: (.+)$/gm))
18299
+ paths.push(match[1].trim());
18300
+ return [...new Set(paths)];
18301
+ };
17059
18302
  // packages/workit-core/src/hooks/handle.ts
18303
+ var usable2 = (value) => value === "native" || value === "partial";
18304
+ var NONE3 = { kind: "none" };
17060
18305
  var offered = new Set;
18306
+ var CLAUDE_WORKTREE_IMPLEMENTER = "workit:implementer";
17061
18307
  var CLAUDE_JUDGES = new Set(["workit:verifier", "workit:reviewer"]);
17062
- // packages/workit-core/src/hooks/hosts/fields.ts
17063
- var SHELLS = new Set(["sh", "bash", "zsh", "dash", "pwsh", "powershell", "cmd"]);
17064
-
18308
+ var subagentSession = (session, agentId) => [session, agentId].filter((part) => Boolean(part)).join(":").replace(/[^A-Za-z0-9_.:@/+-]/g, "-").slice(0, 128);
18309
+ var subagentStartText = (host, session, descriptor, event) => {
18310
+ if (host === "claude_code" && event.agentType === CLAUDE_WORKTREE_IMPLEMENTER)
18311
+ return `Workit observed ${descriptor.label} subagent ${event.agentId} (${event.agentType}) working in its own git worktree: it may edit and commit there, within its brief's scope. Before the first commit, switch to a policy-compliant branch with \`workit git branch <branch> --base <base>\` (e.g. feature/<slug>); branch policy hooks still deny protected or non-compliant branches. Never record a verdict on your own work. Never push, open a PR, or merge unless the brief asks for it.`;
18312
+ const readOnly = `Workit observed ${descriptor.label} subagent ${event.agentId} (${event.agentType}) as read-only/agent-guided.`;
18313
+ if (host === "claude_code" && event.agentType && CLAUDE_JUDGES.has(event.agentType) && event.agentId)
18314
+ return `${readOnly} Its own Workit session is ${subagentSession(session, event.agentId)}: record verdicts with \`workit ledger verdict <result> --session ${subagentSession(session, event.agentId)} ...\` so the ledger tells it apart from the author.`;
18315
+ return readOnly;
18316
+ };
18317
+ function handleHook(input, deps) {
18318
+ const { descriptor } = deps;
18319
+ const event = input.event;
18320
+ switch (event.kind) {
18321
+ case "session.start": {
18322
+ if (!usable2(descriptor.context.sessionStart))
18323
+ return NONE3;
18324
+ const key = input.session.id ? `${input.host}\x00${input.session.id}` : null;
18325
+ const offer = event.source === "startup" && (key === null || !offered.has(key));
18326
+ if (offer && key !== null)
18327
+ offered.add(key);
18328
+ const advisory = usable2(descriptor.events["write.pre"].support) ? null : `Workit cannot gate file writes on ${descriptor.label} (no pre-write hook): before-write requirements (an open product choice, a missing plan) are advisory here, so settle them before editing.`;
18329
+ const addendum = [deps.addendum, advisory].filter(Boolean).join(`
18330
+ `) || null;
18331
+ return {
18332
+ kind: "context",
18333
+ text: sessionContextText(input, descriptor, { offer, addendum })
18334
+ };
18335
+ }
18336
+ case "context.turn": {
18337
+ if (!usable2(descriptor.context.perTurn))
18338
+ return NONE3;
18339
+ const text = turnContextText(input, descriptor);
18340
+ return text ? { kind: "context", text } : NONE3;
18341
+ }
18342
+ case "shell.pre": {
18343
+ const policy = usable2(descriptor.shellPolicy.deny) ? shellPolicy(input.cwd, event.command) : NONE3;
18344
+ if (policy.kind !== "none" || !usable2(descriptor.events["write.pre"].support))
18345
+ return policy;
18346
+ const writes = shellWrites(event.command);
18347
+ return writes.writes ? writeGate(input.cwd, writes.targets) : NONE3;
18348
+ }
18349
+ case "write.pre":
18350
+ return usable2(descriptor.events["write.pre"].support) ? writeGate(input.cwd, event.paths) : NONE3;
18351
+ case "subagent.start":
18352
+ return {
18353
+ kind: "context",
18354
+ text: subagentStartText(input.host, input.session.id ?? null, descriptor, event)
18355
+ };
18356
+ case "compact.pre":
18357
+ return {
18358
+ kind: "notice",
18359
+ userMessage: "Workit context may be stale after compaction; re-run inspection or resume before acting."
18360
+ };
18361
+ case "tool.pre":
18362
+ case "shell.post":
18363
+ case "subagent.stop":
18364
+ case "prompt.submit":
18365
+ case "stop":
18366
+ return NONE3;
18367
+ }
18368
+ }
17065
18369
  // packages/workit-core/src/hooks/hosts/claude-code.ts
17066
18370
  var SOURCES = new Set(["startup", "resume", "clear", "compact", "fork"]);
17067
18371
  var SHELL_TOOLS = new Set(["Bash", "PowerShell"]);
@@ -17088,6 +18392,7 @@ var PI_DESCRIPTOR = {
17088
18392
  "context.turn": { support: "native", native: "before_agent_start" },
17089
18393
  "shell.pre": { support: "native", native: "tool_call" },
17090
18394
  "tool.pre": { support: "native", native: "tool_call" },
18395
+ "write.pre": { support: "native", native: "tool_call" },
17091
18396
  "shell.post": { support: "native", native: "tool_result" },
17092
18397
  "subagent.start": { support: "none", native: null },
17093
18398
  "subagent.stop": { support: "none", native: null },
@@ -17206,7 +18511,7 @@ var output = (result) => ({
17206
18511
  content: [{ type: "text", text: JSON.stringify(result) }],
17207
18512
  details: result
17208
18513
  });
17209
- var schemaFor = (family) => ({ type: "object", ...boundedOperationJsonSchema(family, OPERATION_SCHEMA_DEPTH, true) });
18514
+ var schemaFor = (family) => flatOperationJsonSchema(family, { stringTolerant: true });
17210
18515
  var decodeStrings = (value) => {
17211
18516
  if (typeof value === "string") {
17212
18517
  try {
@@ -17228,10 +18533,9 @@ var trustedForMutation = (ctx, action) => {
17228
18533
  return failure2("permission_denied", "Pi project is not trusted for Workit mutations");
17229
18534
  };
17230
18535
  var executeFamily = async (family, input, ctx) => {
17231
- const parsedStrict = parseOperation(family, input);
17232
- const parsed = parsedStrict.ok ? parsedStrict : parseOperation(family, decodeStrings(input));
18536
+ const parsed = parseAdvertisedOperation(family, input, "pi");
17233
18537
  if (!parsed.ok)
17234
- return output(parsedStrict);
18538
+ return output(parsed);
17235
18539
  const trust = trustedForMutation(ctx, parsed.data.action);
17236
18540
  if (!trust.ok)
17237
18541
  return output(trust);
@@ -17244,7 +18548,7 @@ var registerWorkitTools = (pi, options = {}) => {
17244
18548
  pi.registerTool({
17245
18549
  name: `workit_${family}`,
17246
18550
  label: `workit_${family}`,
17247
- description: `Workit ${family} operations backed by the shared task contract.`,
18551
+ description: operationDescription(family),
17248
18552
  parameters: schemaFor(family),
17249
18553
  async execute(_toolCallId, input, _signal, _onUpdate, ctx) {
17250
18554
  return executeFamily(family, input, ctx);
@@ -17266,27 +18570,42 @@ var registerWorkitTools = (pi, options = {}) => {
17266
18570
  });
17267
18571
  };
17268
18572
  var enforceToolPolicy = (event, ctx) => {
18573
+ const gate = (hookEvent) => {
18574
+ const decision = handleHook({
18575
+ host: "pi",
18576
+ cwd: ctx.cwd,
18577
+ session: { id: "", agentId: null, agentType: null, parentId: null },
18578
+ permissionMode: null,
18579
+ transcriptPath: null,
18580
+ event: hookEvent
18581
+ }, { descriptor: PI_DESCRIPTOR, addendum: null });
18582
+ return decision.kind === "deny" ? { block: true, reason: decision.reason } : undefined;
18583
+ };
17269
18584
  if (event.toolName === "bash") {
17270
18585
  const command = event.input?.command;
17271
- const decision = typeof command === "string" ? shellPolicy(ctx.cwd, command) : null;
17272
- return decision?.kind === "deny" ? { block: true, reason: decision.reason } : undefined;
18586
+ return typeof command === "string" ? gate({ kind: "shell.pre", command, toolUseId: null }) : undefined;
17273
18587
  }
17274
18588
  if (event.toolName !== "write" && event.toolName !== "edit")
17275
18589
  return;
17276
18590
  if (!ctx.isProjectTrusted())
17277
18591
  return { block: true, reason: "Pi project is not trusted" };
17278
- return;
18592
+ return gate({
18593
+ kind: "write.pre",
18594
+ tool: event.toolName,
18595
+ paths: writePaths(event.input),
18596
+ toolUseId: null
18597
+ });
17279
18598
  };
17280
18599
 
17281
18600
  // packages/workit-pi/src/launches.ts
17282
18601
  import { mkdirSync as mkdirSync3, readFileSync as readFileSync6, readdirSync as readdirSync5, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "node:fs";
17283
- import os3 from "node:os";
17284
- import path18 from "node:path";
18602
+ import os4 from "node:os";
18603
+ import path22 from "node:path";
17285
18604
  var launchDir = (root) => {
17286
18605
  const location = resolveStore(root);
17287
- return location instanceof Error ? null : path18.join(location.dir, "pi-launches");
18606
+ return location instanceof Error ? null : path22.join(location.dir, "pi-launches");
17288
18607
  };
17289
- var fileFor = (dir, workerId) => path18.join(dir, `${workerId.replace(/[^A-Za-z0-9-]/g, "_")}.json`);
18608
+ var fileFor = (dir, workerId) => path22.join(dir, `${workerId.replace(/[^A-Za-z0-9-]/g, "_")}.json`);
17290
18609
  var recordLaunch = (root, attempt) => {
17291
18610
  const dir = launchDir(root);
17292
18611
  if (!dir)
@@ -17296,7 +18615,7 @@ var recordLaunch = (root, attempt) => {
17296
18615
  writeFileSync3(fileFor(dir, attempt.workerId), JSON.stringify({
17297
18616
  ...attempt,
17298
18617
  pid: process.pid,
17299
- host: os3.hostname(),
18618
+ host: os4.hostname(),
17300
18619
  at: new Date().toISOString()
17301
18620
  }), { flag: "wx" });
17302
18621
  return true;
@@ -17310,7 +18629,7 @@ var clearLaunch = (root, workerId) => {
17310
18629
  rmSync2(fileFor(dir, workerId), { force: true });
17311
18630
  };
17312
18631
  var alive = (attempt) => {
17313
- if (attempt.host !== os3.hostname())
18632
+ if (attempt.host !== os4.hostname())
17314
18633
  return true;
17315
18634
  try {
17316
18635
  process.kill(attempt.pid, 0);
@@ -17332,7 +18651,7 @@ var abandonedLaunches = (root) => {
17332
18651
  const found = [];
17333
18652
  for (const name of names) {
17334
18653
  try {
17335
- const attempt = JSON.parse(readFileSync6(path18.join(dir, name), "utf8"));
18654
+ const attempt = JSON.parse(readFileSync6(path22.join(dir, name), "utf8"));
17336
18655
  if (typeof attempt.workerId === "string" && typeof attempt.session === "string") {
17337
18656
  if (!alive(attempt))
17338
18657
  found.push(attempt);
@@ -17350,9 +18669,9 @@ import { randomUUID as randomUUID4 } from "node:crypto";
17350
18669
  // packages/workit-pi/src/worker-protocol.ts
17351
18670
  var MAX_WORKER_LINE_BYTES = 64 * 1024;
17352
18671
  var MAX_WORKER_STDERR_BYTES = 64 * 1024;
17353
- var isRecord3 = (value) => typeof value === "object" && value !== null;
18672
+ var isRecord4 = (value) => typeof value === "object" && value !== null;
17354
18673
  var parseEvent2 = (value) => {
17355
- if (!isRecord3(value) || typeof value.type !== "string")
18674
+ if (!isRecord4(value) || typeof value.type !== "string")
17356
18675
  return null;
17357
18676
  if (value.type === "workit_worker_ready") {
17358
18677
  if (typeof value.workerId !== "string" || typeof value.sessionId !== "string")