@brainervirus/workit-codex 1.0.12 → 1.1.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/.codex-plugin/plugin.json +1 -1
- package/dist/launch-mcp.js +1701 -1517
- package/dist/workit-hook.js +422 -55
- package/package.json +3 -3
package/dist/launch-mcp.js
CHANGED
|
@@ -14199,7 +14199,13 @@ var refSchema = discriminatedUnion("kind", [
|
|
|
14199
14199
|
id
|
|
14200
14200
|
}).strict(),
|
|
14201
14201
|
object({ kind: literal("host"), host: hostSchema, handle: nonEmpty }).strict(),
|
|
14202
|
-
object({ kind: literal("external"), url: text.url() }).strict()
|
|
14202
|
+
object({ kind: literal("external"), url: text.url() }).strict(),
|
|
14203
|
+
object({
|
|
14204
|
+
kind: literal("standing"),
|
|
14205
|
+
workspace: nonEmpty,
|
|
14206
|
+
class: nonEmpty,
|
|
14207
|
+
configDigest: nullableDigest
|
|
14208
|
+
}).strict()
|
|
14203
14209
|
]);
|
|
14204
14210
|
var rewriteUnknown = (value, map) => {
|
|
14205
14211
|
if (Array.isArray(value)) {
|
|
@@ -14504,7 +14510,8 @@ var decisionSchema = object({
|
|
|
14504
14510
|
approvedContent: text,
|
|
14505
14511
|
displayed: text.optional(),
|
|
14506
14512
|
contentRefs: array(refSchema),
|
|
14507
|
-
statedChoice: object({ ref: text, text }).strict().optional()
|
|
14513
|
+
statedChoice: object({ ref: text, text }).strict().optional(),
|
|
14514
|
+
standing: object({ workspace: nonEmpty, class: nonEmpty }).strict().optional()
|
|
14508
14515
|
}).strict(),
|
|
14509
14516
|
digest,
|
|
14510
14517
|
response: _enum(["approved", "rejected", "stated"]),
|
|
@@ -16580,1622 +16587,1784 @@ function assertProductWriteAllowed(input) {
|
|
|
16580
16587
|
}
|
|
16581
16588
|
|
|
16582
16589
|
// packages/workit-core/src/core/task-evaluation.ts
|
|
16590
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
16591
|
+
import * as fs7 from "node:fs";
|
|
16592
|
+
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
16593
|
+
import path9 from "node:path";
|
|
16594
|
+
|
|
16595
|
+
// packages/workit-core/src/core/authority.ts
|
|
16583
16596
|
import { createHash as createHash4 } from "node:crypto";
|
|
16584
16597
|
import * as fs6 from "node:fs";
|
|
16585
|
-
import
|
|
16586
|
-
import path6 from "node:path";
|
|
16598
|
+
import path8 from "node:path";
|
|
16587
16599
|
|
|
16588
|
-
// packages/workit-core/src/core/
|
|
16600
|
+
// packages/workit-core/src/core/auto-approval.ts
|
|
16589
16601
|
import { createHash as createHash3 } from "node:crypto";
|
|
16590
|
-
import
|
|
16602
|
+
import { readFileSync as readFileSync4 } from "node:fs";
|
|
16603
|
+
|
|
16604
|
+
// packages/workit-core/src/core/workspaces.ts
|
|
16605
|
+
import { readFileSync as readFileSync3, realpathSync as realpathSync2 } from "node:fs";
|
|
16606
|
+
import path6 from "node:path";
|
|
16607
|
+
|
|
16608
|
+
// packages/workit-core/src/core/config.ts
|
|
16609
|
+
import {
|
|
16610
|
+
copyFileSync,
|
|
16611
|
+
cpSync,
|
|
16612
|
+
existsSync as existsSync3,
|
|
16613
|
+
mkdirSync as mkdirSync2,
|
|
16614
|
+
readFileSync as readFileSync2,
|
|
16615
|
+
readdirSync as readdirSync2,
|
|
16616
|
+
writeFileSync as writeFileSync2
|
|
16617
|
+
} from "node:fs";
|
|
16618
|
+
import os from "node:os";
|
|
16591
16619
|
import path5 from "node:path";
|
|
16592
|
-
|
|
16593
|
-
|
|
16594
|
-
|
|
16595
|
-
|
|
16596
|
-
|
|
16620
|
+
|
|
16621
|
+
// packages/workit-core/src/core/boundary.ts
|
|
16622
|
+
import { statSync } from "node:fs";
|
|
16623
|
+
var EVENT = {
|
|
16624
|
+
initialization: "initialization",
|
|
16625
|
+
provenance: "provenance",
|
|
16626
|
+
assets: "assets",
|
|
16627
|
+
configurationSource: "configuration_source",
|
|
16628
|
+
hooks: "hooks",
|
|
16629
|
+
mcpConnection: "mcp_connection",
|
|
16630
|
+
migration: "migration",
|
|
16631
|
+
installSteps: "install_steps",
|
|
16632
|
+
uncaughtFailure: "uncaught_failure",
|
|
16633
|
+
toolsFailed: "tools_failed",
|
|
16634
|
+
doctor: "doctor"
|
|
16597
16635
|
};
|
|
16598
|
-
var
|
|
16599
|
-
|
|
16600
|
-
|
|
16601
|
-
|
|
16602
|
-
|
|
16603
|
-
|
|
16604
|
-
|
|
16605
|
-
|
|
16606
|
-
|
|
16607
|
-
|
|
16608
|
-
|
|
16609
|
-
|
|
16610
|
-
|
|
16611
|
-
|
|
16612
|
-
|
|
16613
|
-
|
|
16614
|
-
|
|
16615
|
-
|
|
16616
|
-
|
|
16617
|
-
|
|
16618
|
-
|
|
16636
|
+
var errorDetail = (err) => {
|
|
16637
|
+
if (err instanceof Error) {
|
|
16638
|
+
return { error_name: err.name, error: err.message };
|
|
16639
|
+
}
|
|
16640
|
+
return { error_name: "unknown", error: String(err) };
|
|
16641
|
+
};
|
|
16642
|
+
var markSourcesLoaded = (files, loadedAt = Date.now()) => ({
|
|
16643
|
+
loadedAt,
|
|
16644
|
+
files: [...files]
|
|
16645
|
+
});
|
|
16646
|
+
var changedSourcesSinceLoad = (marker) => {
|
|
16647
|
+
const changed = [];
|
|
16648
|
+
for (const file of marker.files) {
|
|
16649
|
+
try {
|
|
16650
|
+
if (statSync(file).mtimeMs > marker.loadedAt)
|
|
16651
|
+
changed.push(file);
|
|
16652
|
+
} catch {}
|
|
16653
|
+
}
|
|
16654
|
+
return changed;
|
|
16655
|
+
};
|
|
16656
|
+
|
|
16657
|
+
// packages/workit-core/src/core/config.ts
|
|
16658
|
+
var diagnosticLogger;
|
|
16659
|
+
var PRESETS = {
|
|
16660
|
+
gitflow: {
|
|
16661
|
+
allowed: ["feature/*", "bugfix/*", "hotfix/*", "release/*"],
|
|
16662
|
+
protected: ["main", "develop", "master", "prod", "production"]
|
|
16663
|
+
},
|
|
16664
|
+
"github-flow": { allowed: ["*"], protected: ["main"] },
|
|
16665
|
+
"trunk-based": { allowed: ["*"], protected: ["main"] },
|
|
16666
|
+
custom: { allowed: [], protected: [] }
|
|
16667
|
+
};
|
|
16668
|
+
var mergePreset = (preset, input = {}, current = {
|
|
16669
|
+
branchPolicy: { preset, allowed: [], protected: [] }
|
|
16670
|
+
}) => {
|
|
16671
|
+
const defs = PRESETS[preset];
|
|
16672
|
+
return {
|
|
16673
|
+
preset,
|
|
16674
|
+
allowed: preset === "custom" ? input.allowed ?? current.branchPolicy.allowed : [...defs.allowed],
|
|
16675
|
+
protected: preset === "custom" ? input.protectedNames ?? current.branchPolicy.protected : [...defs.protected]
|
|
16676
|
+
};
|
|
16677
|
+
};
|
|
16678
|
+
var resolveConfigDir = () => process.env.WORKFLOW_TOOLKIT_CONFIG ?? process.env.WORKFLOW_TOOLKIT_CONFIG_DIR ?? path5.join(process.env.XDG_CONFIG_HOME || path5.join(os.homedir(), ".config"), "workit");
|
|
16679
|
+
var migratedDir = null;
|
|
16680
|
+
var migrationFailed = false;
|
|
16681
|
+
var ensureConfigDir = (dir = resolveConfigDir()) => {
|
|
16682
|
+
if (migratedDir === dir)
|
|
16683
|
+
return dir;
|
|
16684
|
+
if (process.env.WORKFLOW_TOOLKIT_CONFIG || process.env.WORKFLOW_TOOLKIT_CONFIG_DIR) {
|
|
16685
|
+
migratedDir = dir;
|
|
16686
|
+
return dir;
|
|
16687
|
+
}
|
|
16688
|
+
const legacy = path5.join(process.env.XDG_CONFIG_HOME || path5.join(os.homedir(), ".config"), "workflow-toolkit");
|
|
16689
|
+
if (!existsSync3(legacy)) {
|
|
16690
|
+
migratedDir = dir;
|
|
16691
|
+
return dir;
|
|
16692
|
+
}
|
|
16693
|
+
if (!migrationFailed && existsSync3(dir)) {
|
|
16694
|
+
migratedDir = dir;
|
|
16695
|
+
return dir;
|
|
16696
|
+
}
|
|
16697
|
+
migrationFailed = false;
|
|
16698
|
+
mkdirSync2(dir, { recursive: true });
|
|
16699
|
+
diagnosticLogger?.info(EVENT.migration, { from: legacy, to: dir });
|
|
16700
|
+
for (const entry of readdirSync2(legacy, { withFileTypes: true })) {
|
|
16701
|
+
const src = path5.join(legacy, entry.name);
|
|
16702
|
+
const dest = path5.join(dir, entry.name);
|
|
16703
|
+
if (existsSync3(dest))
|
|
16619
16704
|
continue;
|
|
16705
|
+
try {
|
|
16706
|
+
if (entry.isDirectory())
|
|
16707
|
+
cpSync(src, dest, { recursive: true });
|
|
16708
|
+
else if (entry.isFile())
|
|
16709
|
+
copyFileSync(src, dest);
|
|
16710
|
+
} catch (err) {
|
|
16711
|
+
migrationFailed = true;
|
|
16712
|
+
diagnosticLogger?.warn(EVENT.migration, { from: src, ok: false, ...errorDetail(err) });
|
|
16713
|
+
console.warn(`[workit] config migration: a file could not be copied to ${dir}; it will be retried on the next run`);
|
|
16620
16714
|
}
|
|
16621
|
-
return null;
|
|
16622
16715
|
}
|
|
16623
|
-
|
|
16624
|
-
|
|
16716
|
+
if (!migrationFailed)
|
|
16717
|
+
migratedDir = dir;
|
|
16718
|
+
return dir;
|
|
16719
|
+
};
|
|
16720
|
+
var configDir = () => ensureConfigDir();
|
|
16721
|
+
var LOCALE_RE = /^[a-z]{2,3}(-(?:[A-Z]{2}|[0-9]{3}))?$/;
|
|
16722
|
+
var DEFAULTS = {
|
|
16723
|
+
locale: "en",
|
|
16724
|
+
localeOptions: ["en", "es-CL", "es-MX", "es-AR", "pt-BR"],
|
|
16725
|
+
timezone: "America/Santiago",
|
|
16726
|
+
branchPolicy: {
|
|
16727
|
+
preset: "gitflow",
|
|
16728
|
+
allowed: [...PRESETS.gitflow.allowed],
|
|
16729
|
+
protected: [...PRESETS.gitflow.protected]
|
|
16730
|
+
},
|
|
16731
|
+
commitPolicy: { preset: "conventional" }
|
|
16732
|
+
};
|
|
16733
|
+
var readSafe = (p) => {
|
|
16734
|
+
try {
|
|
16735
|
+
return readFileSync2(p, "utf8");
|
|
16736
|
+
} catch {
|
|
16625
16737
|
return null;
|
|
16626
|
-
|
|
16738
|
+
}
|
|
16627
16739
|
};
|
|
16628
|
-
var
|
|
16629
|
-
var
|
|
16630
|
-
|
|
16740
|
+
var isConfigObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
16741
|
+
var parseConfigResult = (raw, file) => {
|
|
16742
|
+
if (raw === null)
|
|
16743
|
+
return { status: "missing", path: file };
|
|
16744
|
+
let parsed;
|
|
16631
16745
|
try {
|
|
16632
|
-
|
|
16746
|
+
parsed = JSON.parse(raw);
|
|
16633
16747
|
} catch {
|
|
16634
|
-
return
|
|
16748
|
+
return { status: "malformed", path: file, error: `${file} is not valid JSON` };
|
|
16635
16749
|
}
|
|
16636
|
-
if (
|
|
16637
|
-
return
|
|
16638
|
-
|
|
16639
|
-
|
|
16640
|
-
|
|
16641
|
-
const
|
|
16642
|
-
|
|
16750
|
+
if (!isConfigObject(parsed)) {
|
|
16751
|
+
return { status: "malformed", path: file, error: `${file} is not a JSON object` };
|
|
16752
|
+
}
|
|
16753
|
+
const input = parsed;
|
|
16754
|
+
const locale = LOCALE_RE.test(String(input.locale ?? "")) ? input.locale : DEFAULTS.locale;
|
|
16755
|
+
const preset = Object.hasOwn(PRESETS, input.branchPolicy?.preset) ? input.branchPolicy?.preset : "gitflow";
|
|
16756
|
+
const commitPreset = COMMIT_PRESETS.includes(String(input.commitPolicy?.preset ?? "")) ? input.commitPolicy?.preset : "conventional";
|
|
16757
|
+
return {
|
|
16758
|
+
status: "valid",
|
|
16759
|
+
path: file,
|
|
16760
|
+
config: {
|
|
16761
|
+
locale,
|
|
16762
|
+
localeOptions: Array.isArray(input.localeOptions) ? input.localeOptions : DEFAULTS.localeOptions,
|
|
16763
|
+
timezone: input.timezone ?? DEFAULTS.timezone,
|
|
16764
|
+
branchPolicy: mergePreset(preset, {
|
|
16765
|
+
allowed: Array.isArray(input.branchPolicy?.allowed) ? input.branchPolicy.allowed : undefined,
|
|
16766
|
+
protectedNames: Array.isArray(input.branchPolicy?.protected) ? input.branchPolicy.protected : undefined
|
|
16767
|
+
}, DEFAULTS),
|
|
16768
|
+
commitPolicy: {
|
|
16769
|
+
preset: commitPreset,
|
|
16770
|
+
...typeof input.commitPolicy?.pattern === "string" ? { pattern: input.commitPolicy.pattern } : {}
|
|
16771
|
+
}
|
|
16772
|
+
}
|
|
16773
|
+
};
|
|
16643
16774
|
};
|
|
16644
|
-
var
|
|
16645
|
-
const
|
|
16646
|
-
return
|
|
16775
|
+
var readConfigTyped = (dir) => {
|
|
16776
|
+
const file = path5.join(dir ?? configDir(), "config.json");
|
|
16777
|
+
return parseConfigResult(readSafe(file), file);
|
|
16647
16778
|
};
|
|
16648
|
-
var
|
|
16649
|
-
|
|
16650
|
-
if (
|
|
16651
|
-
|
|
16652
|
-
|
|
16653
|
-
return provenance.kind === "host_observed" && provenance.host === caller.host && provenance.session?.kind === "host" && provenance.session.host === caller.host && provenance.session.handle === caller.actor && provenance.receipts.some((receipt) => receipt.kind === "host" && receipt.host === caller.host);
|
|
16779
|
+
var readConfig = () => {
|
|
16780
|
+
const result = readConfigTyped();
|
|
16781
|
+
if (result.status === "malformed")
|
|
16782
|
+
throw new Error(result.error);
|
|
16783
|
+
return result.config ?? DEFAULTS;
|
|
16654
16784
|
};
|
|
16655
|
-
var
|
|
16656
|
-
|
|
16657
|
-
|
|
16658
|
-
const
|
|
16659
|
-
|
|
16660
|
-
|
|
16661
|
-
|
|
16662
|
-
|
|
16663
|
-
|
|
16664
|
-
|
|
16665
|
-
|
|
16666
|
-
|
|
16667
|
-
|
|
16668
|
-
|
|
16669
|
-
|
|
16670
|
-
}, binding, input.caller));
|
|
16785
|
+
var resolveBranchPolicy = (config, workspace) => {
|
|
16786
|
+
const wp = workspace?.branchPolicy ?? {};
|
|
16787
|
+
const preset = Object.hasOwn(PRESETS, wp.preset) ? wp.preset : config.branchPolicy?.preset ?? "gitflow";
|
|
16788
|
+
const merged = mergePreset(preset, {
|
|
16789
|
+
allowed: wp.allowed,
|
|
16790
|
+
protectedNames: wp.protected
|
|
16791
|
+
}, config);
|
|
16792
|
+
const allowed = merged.allowed.map((p) => new RegExp(`^${p.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*")}$`, "i"));
|
|
16793
|
+
return {
|
|
16794
|
+
preset,
|
|
16795
|
+
allowed,
|
|
16796
|
+
protected: new Set(merged.protected.map((p) => p.toLowerCase())),
|
|
16797
|
+
integration: wp.integration === "merge" ? "merge" : "pr",
|
|
16798
|
+
defaultTargetBranch: preset === "github-flow" ? "main" : preset === "trunk-based" ? "master" : "develop"
|
|
16799
|
+
};
|
|
16671
16800
|
};
|
|
16672
|
-
var
|
|
16673
|
-
|
|
16674
|
-
|
|
16675
|
-
|
|
16676
|
-
|
|
16677
|
-
|
|
16678
|
-
|
|
16679
|
-
|
|
16680
|
-
|
|
16681
|
-
|
|
16682
|
-
|
|
16683
|
-
|
|
16684
|
-
|
|
16685
|
-
|
|
16686
|
-
|
|
16687
|
-
|
|
16688
|
-
|
|
16689
|
-
|
|
16690
|
-
|
|
16691
|
-
|
|
16692
|
-
|
|
16693
|
-
|
|
16694
|
-
|
|
16695
|
-
|
|
16696
|
-
|
|
16697
|
-
|
|
16698
|
-
|
|
16699
|
-
|
|
16700
|
-
|
|
16701
|
-
|
|
16702
|
-
|
|
16703
|
-
|
|
16704
|
-
|
|
16705
|
-
workspaceRevision: input.expected.workspaceRevision,
|
|
16706
|
-
outcome: input.expected.outcome,
|
|
16707
|
-
evidenceDigest: input.expected.evidenceDigest,
|
|
16708
|
-
step: input.expected.step,
|
|
16709
|
-
purpose: input.expected.decision.purpose,
|
|
16710
|
-
response: input.expected.decision.response,
|
|
16711
|
-
binding: input.expected.decision.binding,
|
|
16712
|
-
digest: input.expected.decision.digest,
|
|
16713
|
-
requirementIds: [...input.expected.decision.requirementIds]
|
|
16714
|
-
}, binding, input.caller));
|
|
16715
|
-
};
|
|
16716
|
-
var takeAuthority = (authority, binding, caller) => {
|
|
16717
|
-
const record = verifiedAuthorities.get(authority);
|
|
16718
|
-
verifiedAuthorities.delete(authority);
|
|
16719
|
-
if (!record)
|
|
16720
|
-
return null;
|
|
16721
|
-
if (record.owner !== binding.owner || record.store !== binding.store || record.root !== binding.root || record.root !== record.store.root || canonicalJson(record.caller) !== canonicalJson(caller))
|
|
16722
|
-
return null;
|
|
16723
|
-
return record;
|
|
16801
|
+
var COMMIT_PRESETS = [
|
|
16802
|
+
"conventional",
|
|
16803
|
+
"gitmoji",
|
|
16804
|
+
"ticket-prefix",
|
|
16805
|
+
"freeform",
|
|
16806
|
+
"custom",
|
|
16807
|
+
"auto"
|
|
16808
|
+
];
|
|
16809
|
+
|
|
16810
|
+
// packages/workit-core/src/core/workspaces.ts
|
|
16811
|
+
var readWorkspacesResult = (dir = configDir()) => {
|
|
16812
|
+
const file = path6.join(dir, "workspaces.json");
|
|
16813
|
+
let raw;
|
|
16814
|
+
try {
|
|
16815
|
+
raw = readFileSync3(file, "utf8");
|
|
16816
|
+
} catch {
|
|
16817
|
+
return { status: "missing", path: file, entries: [] };
|
|
16818
|
+
}
|
|
16819
|
+
let parsed;
|
|
16820
|
+
try {
|
|
16821
|
+
parsed = JSON.parse(raw);
|
|
16822
|
+
} catch {
|
|
16823
|
+
return { status: "malformed", path: file, entries: [], error: `${file} is not valid JSON` };
|
|
16824
|
+
}
|
|
16825
|
+
if (!isConfigObject(parsed)) {
|
|
16826
|
+
return { status: "malformed", path: file, entries: [], error: `${file} is not a JSON object` };
|
|
16827
|
+
}
|
|
16828
|
+
const list = parsed.workspaces;
|
|
16829
|
+
return {
|
|
16830
|
+
status: "valid",
|
|
16831
|
+
path: file,
|
|
16832
|
+
entries: Array.isArray(list) ? list : []
|
|
16833
|
+
};
|
|
16724
16834
|
};
|
|
16725
|
-
var
|
|
16726
|
-
|
|
16727
|
-
|
|
16728
|
-
|
|
16835
|
+
var loadWorkspacesFrom = (dir) => readWorkspacesResult(dir).entries;
|
|
16836
|
+
var globToRegExp = (glob) => {
|
|
16837
|
+
let out = "";
|
|
16838
|
+
for (let i = 0;i < glob.length; i++) {
|
|
16839
|
+
const c = glob[i];
|
|
16840
|
+
if (c === "*") {
|
|
16841
|
+
if (glob[i + 1] === "*") {
|
|
16842
|
+
if (glob[i + 2] === "/") {
|
|
16843
|
+
if (out === "")
|
|
16844
|
+
out += "/?";
|
|
16845
|
+
out += "(?:[^/]+/)*";
|
|
16846
|
+
i += 2;
|
|
16847
|
+
} else {
|
|
16848
|
+
if (i + 2 >= glob.length) {
|
|
16849
|
+
if (out.endsWith("/")) {
|
|
16850
|
+
out = out.slice(0, -1);
|
|
16851
|
+
out += "(?:/.*)?";
|
|
16852
|
+
} else {
|
|
16853
|
+
out += ".*";
|
|
16854
|
+
}
|
|
16855
|
+
} else {
|
|
16856
|
+
out += ".*";
|
|
16857
|
+
}
|
|
16858
|
+
i++;
|
|
16859
|
+
}
|
|
16860
|
+
} else {
|
|
16861
|
+
out += "[^/]*";
|
|
16862
|
+
}
|
|
16863
|
+
} else {
|
|
16864
|
+
out += c.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
|
|
16865
|
+
}
|
|
16866
|
+
}
|
|
16867
|
+
return new RegExp(`^${out}$`);
|
|
16729
16868
|
};
|
|
16730
|
-
var
|
|
16731
|
-
var
|
|
16732
|
-
|
|
16733
|
-
function applicableDecision(task, purpose, binding, checkoutRoot) {
|
|
16734
|
-
if (binding.taskId !== task.id || binding.workspaceId !== task.workspaceId)
|
|
16735
|
-
return [];
|
|
16736
|
-
return task.decisions.filter((entry) => entry.provenance.kind !== "imported" && decisionMatches(entry, purpose, binding) && (checkoutRoot ? verifyDecisionContentAtRoot(checkoutRoot, entry.data.binding).ok : entry.data.binding.contentRefs.every((reference) => reference.kind !== "file"))).map((entry) => entry.data);
|
|
16737
|
-
}
|
|
16738
|
-
var digestBytes2 = (bytes) => createHash3("sha256").update(bytes).digest("hex");
|
|
16739
|
-
var verifyDecisionContentAtRoot = (checkoutRoot, binding) => {
|
|
16740
|
-
let root;
|
|
16869
|
+
var matchWorkspace = (glob, target) => globToRegExp(glob.replaceAll("\\", "/")).test(target.replaceAll("\\", "/"));
|
|
16870
|
+
var realpathOf = (p) => {
|
|
16871
|
+
const real = realpathSync2.native ?? realpathSync2;
|
|
16741
16872
|
try {
|
|
16742
|
-
|
|
16873
|
+
return real(p);
|
|
16743
16874
|
} catch {
|
|
16744
|
-
return
|
|
16875
|
+
return p;
|
|
16745
16876
|
}
|
|
16746
|
-
|
|
16747
|
-
|
|
16877
|
+
};
|
|
16878
|
+
var canonicalGlob = (glob) => {
|
|
16879
|
+
const m = /[*?[\]{]/.exec(glob);
|
|
16880
|
+
const prefix = m ? glob.slice(0, m.index) : glob;
|
|
16881
|
+
const rest = m ? glob.slice(m.index) : "";
|
|
16882
|
+
if (!prefix)
|
|
16883
|
+
return glob;
|
|
16884
|
+
const real = realpathOf(prefix.replace(/\/+$/, "") || "/");
|
|
16885
|
+
return real + (prefix.endsWith("/") ? "/" : "") + rest;
|
|
16886
|
+
};
|
|
16887
|
+
var resolveWorkspaceFrom = (cwd, dir) => {
|
|
16888
|
+
const targets = [cwd, realpathOf(cwd)].map((p) => p.replaceAll("\\", "/"));
|
|
16889
|
+
for (const entry of loadWorkspacesFrom(dir)) {
|
|
16890
|
+
if (!entry || typeof entry !== "object")
|
|
16748
16891
|
continue;
|
|
16749
|
-
|
|
16750
|
-
|
|
16751
|
-
|
|
16752
|
-
|
|
16753
|
-
|
|
16754
|
-
|
|
16755
|
-
|
|
16756
|
-
|
|
16757
|
-
|
|
16758
|
-
|
|
16759
|
-
const relative = path5.relative(root, target);
|
|
16760
|
-
let current = root;
|
|
16761
|
-
for (const segment of relative.split(path5.sep)) {
|
|
16762
|
-
if (!segment)
|
|
16763
|
-
continue;
|
|
16764
|
-
current = path5.join(current, segment);
|
|
16765
|
-
const stat = fs5.lstatSync(current);
|
|
16766
|
-
const real = fs5.realpathSync(current);
|
|
16767
|
-
if (real !== root && !real.startsWith(`${root}${path5.sep}`))
|
|
16768
|
-
return failure("invalid_input", "document reference escapes checkout; cite it as an external file:// reference instead", {
|
|
16769
|
-
fields: [
|
|
16770
|
-
{
|
|
16771
|
-
path: "contentRefs.path",
|
|
16772
|
-
reason: `${reference.path} resolves outside the checkout`
|
|
16773
|
-
}
|
|
16774
|
-
]
|
|
16775
|
-
});
|
|
16776
|
-
if (stat.isSymbolicLink())
|
|
16777
|
-
return failure("invalid_input", "document reference uses a symlink");
|
|
16778
|
-
if (current === target && !stat.isFile())
|
|
16779
|
-
return failure("invalid_input", "document reference is not a regular file");
|
|
16780
|
-
if (current !== target && !stat.isDirectory())
|
|
16781
|
-
return failure("invalid_input", "document reference ancestor is not a directory");
|
|
16782
|
-
}
|
|
16783
|
-
if (digestBytes2(fs5.readFileSync(target)) !== reference.digest)
|
|
16784
|
-
return failure("permission_denied", "approved document bytes have changed");
|
|
16785
|
-
} catch {
|
|
16786
|
-
return failure("invalid_input", "approved document is unavailable");
|
|
16892
|
+
const ws = entry;
|
|
16893
|
+
if (typeof ws.glob !== "string" || !ws.glob)
|
|
16894
|
+
continue;
|
|
16895
|
+
const glob = ws.glob.replaceAll("\\", "/");
|
|
16896
|
+
const canonical = canonicalGlob(glob);
|
|
16897
|
+
for (const target of targets) {
|
|
16898
|
+
if (matchWorkspace(glob, target))
|
|
16899
|
+
return ws;
|
|
16900
|
+
if (canonical !== glob && matchWorkspace(canonical, target))
|
|
16901
|
+
return ws;
|
|
16787
16902
|
}
|
|
16788
16903
|
}
|
|
16789
|
-
return
|
|
16904
|
+
return null;
|
|
16790
16905
|
};
|
|
16791
|
-
var
|
|
16792
|
-
|
|
16793
|
-
|
|
16794
|
-
|
|
16795
|
-
|
|
16796
|
-
|
|
16797
|
-
|
|
16798
|
-
|
|
16799
|
-
|
|
16800
|
-
|
|
16801
|
-
|
|
16802
|
-
|
|
16803
|
-
|
|
16804
|
-
|
|
16805
|
-
|
|
16806
|
-
|
|
16807
|
-
|
|
16808
|
-
|
|
16809
|
-
|
|
16810
|
-
|
|
16811
|
-
|
|
16812
|
-
|
|
16813
|
-
|
|
16814
|
-
if (!authorityDecisionMatches(authority, decision))
|
|
16815
|
-
return failure("permission_denied", "native reservation authority does not match decision");
|
|
16816
|
-
if (decision.response !== "approved")
|
|
16817
|
-
return failure("permission_denied", "decision was rejected");
|
|
16818
|
-
if (entry.provenance.kind !== "host_observed" || entry.provenance.receipts.length === 0 || !provenanceMatches(entry.provenance, authority.provenance))
|
|
16819
|
-
return failure("permission_denied", "action approval lacks native receipt assurance");
|
|
16820
|
-
if (decision.revoked)
|
|
16821
|
-
return failure("permission_denied", "decision is revoked");
|
|
16822
|
-
if (decision.digest !== decisionDigest(decision))
|
|
16823
|
-
return failure("permission_denied", "decision binding is invalid");
|
|
16824
|
-
if (decision.binding.taskId !== task.id || decision.binding.workspaceId !== workspaceId)
|
|
16825
|
-
return failure("permission_denied", "decision task or workspace binding is invalid");
|
|
16826
|
-
if (input.binding && canonicalJson(input.binding) !== canonicalJson(decision.binding))
|
|
16827
|
-
return failure("permission_denied", "action binding does not match the approved decision");
|
|
16828
|
-
const content = verifyContentRefs(store, decision.binding);
|
|
16829
|
-
if (!content.ok)
|
|
16830
|
-
return content;
|
|
16831
|
-
const requestedSteps = parseSteps(decision.binding.approvedContent);
|
|
16832
|
-
if (requestedSteps === null)
|
|
16833
|
-
return failure("invalid_input", "bounded action steps must be a unique non-empty array");
|
|
16834
|
-
if (input.steps && canonicalJson(input.steps) !== canonicalJson(requestedSteps.map(chainStepKey)))
|
|
16835
|
-
return failure("permission_denied", "bounded action steps do not match approved content");
|
|
16836
|
-
const stored = task.actionProgress?.find((item) => item.decisionId === input.decisionId);
|
|
16837
|
-
const workflow = stored ? { steps: stored.steps, completed: stored.completedSteps } : { steps: requestedSteps.map(chainStepKey), completed: [] };
|
|
16838
|
-
if (workflow.steps.length && requestedSteps.length && canonicalJson(workflow.steps) !== canonicalJson(requestedSteps.map(chainStepKey)))
|
|
16839
|
-
return failure("permission_denied", "bounded action scope changed");
|
|
16840
|
-
return success(null, null, { entry, workflow });
|
|
16906
|
+
var resolveWorkspace = (cwd) => resolveWorkspaceFrom(cwd, configDir());
|
|
16907
|
+
|
|
16908
|
+
// packages/workit-core/src/core/branch.ts
|
|
16909
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
16910
|
+
|
|
16911
|
+
// packages/workit-core/src/core/git.ts
|
|
16912
|
+
import { execFileSync } from "node:child_process";
|
|
16913
|
+
var run = (cwd, args) => {
|
|
16914
|
+
try {
|
|
16915
|
+
const stdout = execFileSync("git", args, {
|
|
16916
|
+
cwd,
|
|
16917
|
+
encoding: "utf8",
|
|
16918
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
16919
|
+
});
|
|
16920
|
+
return { stdout: stdout.trimEnd(), stderr: "", exitCode: 0 };
|
|
16921
|
+
} catch (error) {
|
|
16922
|
+
const e = error;
|
|
16923
|
+
return {
|
|
16924
|
+
stdout: (e.stdout ?? "").trimEnd(),
|
|
16925
|
+
stderr: (e.stderr ?? "").trimEnd(),
|
|
16926
|
+
exitCode: e.status ?? 1
|
|
16927
|
+
};
|
|
16928
|
+
}
|
|
16841
16929
|
};
|
|
16842
|
-
|
|
16843
|
-
|
|
16844
|
-
|
|
16845
|
-
const
|
|
16846
|
-
|
|
16847
|
-
|
|
16848
|
-
|
|
16849
|
-
}, input.authorityCaller);
|
|
16850
|
-
if (!authority)
|
|
16851
|
-
return failure("permission_denied", "native reservation authority is not verified");
|
|
16852
|
-
const changed = input.store.mutateTask(input.taskId, input.expectedRevision, (current, mutation) => {
|
|
16853
|
-
const workspace = input.store.readWorkspace();
|
|
16854
|
-
if (!workspace.ok)
|
|
16855
|
-
return workspace;
|
|
16856
|
-
if (!workspace.data)
|
|
16857
|
-
return failure("not_found", "workspace not found");
|
|
16858
|
-
if (workspace.data.revision !== input.expectedWorkspaceRevision)
|
|
16859
|
-
return failure("revision_conflict", "workspace revision does not match", {
|
|
16860
|
-
expectedWorkspaceRevision: input.expectedWorkspaceRevision,
|
|
16861
|
-
actualWorkspaceRevision: workspace.data.revision
|
|
16862
|
-
});
|
|
16863
|
-
const valid = validateAction(input.store, current, workspace.data.id, input, authority);
|
|
16864
|
-
if (!valid.ok)
|
|
16865
|
-
return valid;
|
|
16866
|
-
const { entry, workflow } = valid.data;
|
|
16867
|
-
const existing = entry.data.consumption;
|
|
16868
|
-
if (existing?.state === "uncertain")
|
|
16869
|
-
return failure("external_outcome_unknown", "previous action outcome is unknown", {
|
|
16870
|
-
operation: "reserve_action",
|
|
16871
|
-
outcome: "unknown"
|
|
16872
|
-
});
|
|
16873
|
-
if (existing?.state === "consumed")
|
|
16874
|
-
return failure("permission_denied", "action approval is consumed");
|
|
16875
|
-
if (existing?.state === "reserved" && workflow.completed.length === 0)
|
|
16876
|
-
return failure("permission_denied", "action approval is already reserved");
|
|
16877
|
-
const requestedStep = input.step ?? workflow.steps[workflow.completed.length];
|
|
16878
|
-
if (workflow.steps.length && (!requestedStep || requestedStep !== workflow.steps[workflow.completed.length]))
|
|
16879
|
-
return failure("permission_denied", "action step is outside the approved remaining workflow");
|
|
16880
|
-
if (requestedStep && workflow.completed.includes(requestedStep))
|
|
16881
|
-
return failure("permission_denied", "action step was already completed");
|
|
16882
|
-
const nextDecision = {
|
|
16883
|
-
...entry.data,
|
|
16884
|
-
consumption: { state: "reserved", at: mutation.now, actionRef: input.actionRef }
|
|
16885
|
-
};
|
|
16886
|
-
const actionProgress = workflow.steps.length ? [
|
|
16887
|
-
...(current.actionProgress ?? []).filter((progress) => progress.decisionId !== input.decisionId),
|
|
16888
|
-
{
|
|
16889
|
-
decisionId: input.decisionId,
|
|
16890
|
-
steps: [...workflow.steps],
|
|
16891
|
-
completedSteps: [...workflow.completed]
|
|
16892
|
-
}
|
|
16893
|
-
] : current.actionProgress ?? [];
|
|
16894
|
-
return success(mutation.revision, null, {
|
|
16895
|
-
...current,
|
|
16896
|
-
actionProgress,
|
|
16897
|
-
decisions: current.decisions.map((candidate) => candidate.id === input.decisionId ? { ...candidate, data: nextDecision } : candidate)
|
|
16898
|
-
});
|
|
16899
|
-
}, input.native?.now);
|
|
16900
|
-
if (!changed.ok)
|
|
16901
|
-
return changed;
|
|
16902
|
-
const workspace = input.store.readWorkspace();
|
|
16903
|
-
if (!workspace.ok || !workspace.data)
|
|
16904
|
-
return failure("recovery_required", "workspace disappeared after reservation");
|
|
16905
|
-
const entry = changed.data.decisions.find((candidate) => candidate.id === input.decisionId);
|
|
16906
|
-
if (!entry)
|
|
16907
|
-
return failure("recovery_required", "reserved decision disappeared");
|
|
16908
|
-
const workflow = changed.data.actionProgress?.find((progress) => progress.decisionId === input.decisionId) ?? {
|
|
16909
|
-
steps: [],
|
|
16910
|
-
completedSteps: []
|
|
16911
|
-
};
|
|
16912
|
-
return success(changed.data.revision, workspace.data.revision, {
|
|
16913
|
-
taskId: input.taskId,
|
|
16914
|
-
decisionId: input.decisionId,
|
|
16915
|
-
actionRef: input.actionRef,
|
|
16916
|
-
taskRevision: changed.data.revision,
|
|
16917
|
-
workspaceRevision: workspace.data.revision,
|
|
16918
|
-
completedSteps: [...workflow.completedSteps],
|
|
16919
|
-
remainingSteps: workflow.steps.slice(workflow.completedSteps.length)
|
|
16920
|
-
});
|
|
16921
|
-
}
|
|
16922
|
-
function settleAction(input) {
|
|
16923
|
-
if (!input.store || !input.taskRevision || !input.workspaceRevision)
|
|
16924
|
-
return failure("invalid_input", "action settlement preconditions are required");
|
|
16925
|
-
const authority = takeAuthority(input.authority, {
|
|
16926
|
-
owner: input.authorityOwner,
|
|
16927
|
-
store: input.store,
|
|
16928
|
-
root: input.store.root
|
|
16929
|
-
}, input.authorityCaller);
|
|
16930
|
-
if (!authority)
|
|
16931
|
-
return failure("permission_denied", "native settlement authority is not verified");
|
|
16932
|
-
if (!refSchema.safeParse(input.actionRef).success)
|
|
16933
|
-
return failure("invalid_input", "action reference is invalid");
|
|
16934
|
-
if (!authorityMatches(authority, {
|
|
16935
|
-
taskId: input.taskId,
|
|
16936
|
-
workspaceId: authority.workspaceId,
|
|
16937
|
-
decisionId: input.decisionId,
|
|
16938
|
-
actionRef: input.actionRef,
|
|
16939
|
-
taskRevision: input.taskRevision,
|
|
16940
|
-
workspaceRevision: input.workspaceRevision,
|
|
16941
|
-
outcome: input.outcome
|
|
16942
|
-
}))
|
|
16943
|
-
return failure("permission_denied", "native settlement authority is not verified");
|
|
16944
|
-
let outcomeResult = null;
|
|
16945
|
-
const changed = input.store.mutateTask(input.taskId, input.taskRevision, (current, mutation) => {
|
|
16946
|
-
const workspace = input.store.readWorkspace();
|
|
16947
|
-
if (!workspace.ok)
|
|
16948
|
-
return workspace;
|
|
16949
|
-
if (!workspace.data)
|
|
16950
|
-
return failure("not_found", "workspace not found");
|
|
16951
|
-
if (!authorityMatches(authority, {
|
|
16952
|
-
taskId: input.taskId,
|
|
16953
|
-
workspaceId: workspace.data.id,
|
|
16954
|
-
decisionId: input.decisionId,
|
|
16955
|
-
actionRef: input.actionRef,
|
|
16956
|
-
taskRevision: input.taskRevision,
|
|
16957
|
-
workspaceRevision: input.workspaceRevision,
|
|
16958
|
-
outcome: input.outcome
|
|
16959
|
-
}))
|
|
16960
|
-
return failure("permission_denied", "native settlement authority does not match context");
|
|
16961
|
-
if (workspace.data.revision !== input.workspaceRevision)
|
|
16962
|
-
return failure("revision_conflict", "workspace revision does not match", {
|
|
16963
|
-
expectedWorkspaceRevision: input.workspaceRevision,
|
|
16964
|
-
actualWorkspaceRevision: workspace.data.revision
|
|
16965
|
-
});
|
|
16966
|
-
const entry = current.decisions.find((candidate) => candidate.id === input.decisionId);
|
|
16967
|
-
if (!entry)
|
|
16968
|
-
return failure("not_found", "decision not found");
|
|
16969
|
-
if (!authorityDecisionMatches(authority, entry.data))
|
|
16970
|
-
return failure("permission_denied", "native settlement authority does not match decision");
|
|
16971
|
-
if (!provenanceMatches(entry.provenance, authority.provenance))
|
|
16972
|
-
return failure("permission_denied", "native settlement caller does not match approval");
|
|
16973
|
-
if (entry.data.purpose !== "action" || entry.data.digest !== decisionDigest(entry.data))
|
|
16974
|
-
return failure("permission_denied", "decision binding is invalid");
|
|
16975
|
-
if (entry.data.consumption?.state === "uncertain")
|
|
16976
|
-
return failure("external_outcome_unknown", "previous action outcome is unknown", {
|
|
16977
|
-
operation: "settle_action",
|
|
16978
|
-
outcome: "unknown"
|
|
16979
|
-
});
|
|
16980
|
-
if (entry.data.consumption?.state !== "reserved")
|
|
16981
|
-
return failure("permission_denied", "action is not reserved");
|
|
16982
|
-
if (canonicalJson(entry.data.consumption.actionRef) !== canonicalJson(input.actionRef))
|
|
16983
|
-
return failure("permission_denied", "settlement reference does not match reservation");
|
|
16984
|
-
const stored = current.actionProgress?.find((progress) => progress.decisionId === input.decisionId);
|
|
16985
|
-
const workflow = stored ? { steps: stored.steps, completed: stored.completedSteps } : { steps: [], completed: [] };
|
|
16986
|
-
if (entry.data.binding.approvedContent && !stored && (parseSteps(entry.data.binding.approvedContent) ?? []).length)
|
|
16987
|
-
return failure("recovery_required", "bounded action progress is missing");
|
|
16988
|
-
const currentStep = input.step ?? workflow.steps[workflow.completed.length];
|
|
16989
|
-
if (workflow.steps.length && (!currentStep || currentStep !== workflow.steps[workflow.completed.length]))
|
|
16990
|
-
return failure("permission_denied", "settlement step is outside the approved workflow");
|
|
16991
|
-
if (input.outcome === "unknown") {
|
|
16992
|
-
outcomeResult = failure("external_outcome_unknown", "external action outcome is unknown", {
|
|
16993
|
-
operation: "settle_action",
|
|
16994
|
-
outcome: "unknown"
|
|
16995
|
-
});
|
|
16996
|
-
const uncertain = {
|
|
16997
|
-
...entry.data,
|
|
16998
|
-
consumption: { ...entry.data.consumption, state: "uncertain", at: mutation.now }
|
|
16999
|
-
};
|
|
17000
|
-
return success(mutation.revision, null, {
|
|
17001
|
-
...current,
|
|
17002
|
-
decisions: current.decisions.map((candidate) => candidate.id === input.decisionId ? { ...candidate, data: uncertain } : candidate)
|
|
17003
|
-
});
|
|
16930
|
+
var gitContext = (workspaceRoot, paths = []) => {
|
|
16931
|
+
const cwd = workspaceRoot;
|
|
16932
|
+
let failure;
|
|
16933
|
+
const runHere = (args) => {
|
|
16934
|
+
const result = run(cwd, args);
|
|
16935
|
+
if (!failure && result.exitCode !== 0) {
|
|
16936
|
+
failure = { stderr: result.stderr, exitCode: result.exitCode };
|
|
17004
16937
|
}
|
|
17005
|
-
|
|
17006
|
-
|
|
17007
|
-
|
|
17008
|
-
|
|
17009
|
-
|
|
17010
|
-
|
|
16938
|
+
return result.stdout;
|
|
16939
|
+
};
|
|
16940
|
+
const branch = runHere(["rev-parse", "--abbrev-ref", "HEAD"]) || "unknown";
|
|
16941
|
+
const status_short = runHere(["status", "--porcelain"]);
|
|
16942
|
+
const staged = [];
|
|
16943
|
+
const unstaged = [];
|
|
16944
|
+
const untracked = [];
|
|
16945
|
+
for (const line of status_short.split(`
|
|
16946
|
+
`).filter(Boolean)) {
|
|
16947
|
+
const code = line.slice(0, 2);
|
|
16948
|
+
const file = line.slice(3);
|
|
16949
|
+
if (code === "??") {
|
|
16950
|
+
untracked.push(file);
|
|
16951
|
+
} else {
|
|
16952
|
+
if (code[0] !== " " && code[0] !== "?")
|
|
16953
|
+
staged.push(file);
|
|
16954
|
+
if (code[1] !== " " && code[1] !== "?")
|
|
16955
|
+
unstaged.push(file);
|
|
17011
16956
|
}
|
|
17012
|
-
|
|
17013
|
-
|
|
17014
|
-
|
|
17015
|
-
|
|
17016
|
-
|
|
17017
|
-
|
|
16957
|
+
}
|
|
16958
|
+
const pathArgs = paths.length ? ["--", ...paths] : [];
|
|
16959
|
+
const diff_stat = runHere(["diff", "--stat", ...pathArgs]);
|
|
16960
|
+
const cached_stat = runHere(["diff", "--cached", "--stat", ...pathArgs]);
|
|
16961
|
+
const stagedSet = new Set(staged);
|
|
16962
|
+
const unstagedSet = new Set(unstaged);
|
|
16963
|
+
const partial_staged = [...stagedSet].filter((f) => unstagedSet.has(f));
|
|
16964
|
+
return {
|
|
16965
|
+
workspace_root: cwd,
|
|
16966
|
+
branch,
|
|
16967
|
+
status_short,
|
|
16968
|
+
staged,
|
|
16969
|
+
unstaged,
|
|
16970
|
+
untracked,
|
|
16971
|
+
diff_stat: [diff_stat, cached_stat].filter(Boolean).join(`
|
|
16972
|
+
`),
|
|
16973
|
+
partial_staged: partial_staged.length > 0,
|
|
16974
|
+
partial_staged_files: partial_staged,
|
|
16975
|
+
...failure ? { stderr: failure.stderr, exitCode: failure.exitCode } : {}
|
|
16976
|
+
};
|
|
16977
|
+
};
|
|
16978
|
+
|
|
16979
|
+
// packages/workit-core/src/core/vcs-config.ts
|
|
16980
|
+
import fs5 from "node:fs";
|
|
16981
|
+
import path7 from "node:path";
|
|
16982
|
+
import { spawnSync } from "node:child_process";
|
|
16983
|
+
var TOKEN_PLACEHOLDER = "YOUR_TOKEN_HERE";
|
|
16984
|
+
var vcsConfigPath = () => process.env.WORKFLOW_VCS_CONFIG ?? path7.join(configDir(), "vcs.json");
|
|
16985
|
+
var vcsCwd = (cwd) => cwd ?? process.env.WORKFLOW_WORKSPACE_ROOT ?? process.cwd();
|
|
16986
|
+
var remoteProvider = (cwd) => {
|
|
16987
|
+
const r = spawnSync("git", ["remote", "get-url", "origin"], { cwd, encoding: "utf8" });
|
|
16988
|
+
if (r.status !== 0)
|
|
16989
|
+
return null;
|
|
16990
|
+
const url = (r.stdout ?? "").trim();
|
|
16991
|
+
if (/github\.com[:/]/.test(url))
|
|
16992
|
+
return "github";
|
|
16993
|
+
if (/gitlab\.com[:/]/.test(url))
|
|
16994
|
+
return "gitlab";
|
|
16995
|
+
return null;
|
|
16996
|
+
};
|
|
16997
|
+
var readVcsConfig = () => {
|
|
16998
|
+
const file = vcsConfigPath();
|
|
16999
|
+
let raw;
|
|
17000
|
+
try {
|
|
17001
|
+
raw = fs5.readFileSync(file, "utf8");
|
|
17002
|
+
} catch {
|
|
17003
|
+
return { status: "missing", path: file, config: {} };
|
|
17004
|
+
}
|
|
17005
|
+
let parsed;
|
|
17006
|
+
try {
|
|
17007
|
+
parsed = JSON.parse(raw);
|
|
17008
|
+
} catch {
|
|
17009
|
+
return { status: "malformed", path: file, config: {}, error: `${file} is not valid JSON` };
|
|
17010
|
+
}
|
|
17011
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
17012
|
+
return { status: "valid", path: file, config: parsed };
|
|
17013
|
+
}
|
|
17014
|
+
return { status: "malformed", path: file, config: {}, error: `${file} is not a JSON object` };
|
|
17015
|
+
};
|
|
17016
|
+
function vcsConfig(mode, cwd) {
|
|
17017
|
+
const ws = resolveWorkspace(vcsCwd(cwd));
|
|
17018
|
+
const wsVcs = ws?.vcs ?? {};
|
|
17019
|
+
const wsYt = ws?.youtrack ?? {};
|
|
17020
|
+
const wsIssues = ws?.issues ?? {};
|
|
17021
|
+
const { status: cfgStatus, path: cfgPath, config: cfg, error: cfgError } = readVcsConfig();
|
|
17022
|
+
const root = vcsCwd(cwd);
|
|
17023
|
+
const determinateRoot = cwd !== undefined || process.env.WORKFLOW_WORKSPACE_ROOT !== undefined;
|
|
17024
|
+
const rawProvider = wsVcs.provider ?? (determinateRoot ? remoteProvider(root) : null) ?? cfg.provider ?? null;
|
|
17025
|
+
const provider = rawProvider === null || rawProvider === undefined ? null : String(rawProvider).toLowerCase() || null;
|
|
17026
|
+
const wp = ws?.branchPolicy ?? {};
|
|
17027
|
+
const hasWorkspacePolicy = typeof wp.preset === "string" && Object.hasOwn(PRESETS, wp.preset);
|
|
17028
|
+
const policyDefault = resolveBranchPolicyFor(root).defaultTargetBranch;
|
|
17029
|
+
const defaultTarget = String(wsVcs.defaultTargetBranch ?? (hasWorkspacePolicy ? policyDefault : cfg.defaultTargetBranch ?? policyDefault) ?? "develop");
|
|
17030
|
+
const linkIssues = typeof wsYt.link_issues === "boolean" ? wsYt.link_issues : null;
|
|
17031
|
+
const youtrackBaseUrl = typeof wsYt.baseUrl === "string" ? wsYt.baseUrl : null;
|
|
17032
|
+
let issuesProvider = null;
|
|
17033
|
+
let linkOnPr = null;
|
|
17034
|
+
if (provider === "github" && typeof wsIssues.provider === "string" && wsIssues.provider.toLowerCase() === "github") {
|
|
17035
|
+
issuesProvider = "github";
|
|
17036
|
+
linkOnPr = typeof wsIssues.link_on_pr === "boolean" ? wsIssues.link_on_pr : null;
|
|
17037
|
+
}
|
|
17038
|
+
if (mode === "resolve") {
|
|
17039
|
+
if (cfgStatus === "malformed")
|
|
17040
|
+
return { ok: false, error: cfgError, configPath: cfgPath };
|
|
17041
|
+
return {
|
|
17042
|
+
ok: true,
|
|
17043
|
+
workspace_name: ws?.name ?? null,
|
|
17044
|
+
provider,
|
|
17045
|
+
defaultTargetBranch: defaultTarget,
|
|
17046
|
+
link_issues: linkIssues,
|
|
17047
|
+
youtrack_base_url: youtrackBaseUrl,
|
|
17048
|
+
issues_provider: issuesProvider,
|
|
17049
|
+
link_on_pr: linkOnPr
|
|
17018
17050
|
};
|
|
17019
|
-
|
|
17020
|
-
|
|
17021
|
-
|
|
17022
|
-
|
|
17023
|
-
|
|
17024
|
-
|
|
17025
|
-
|
|
17026
|
-
|
|
17027
|
-
|
|
17028
|
-
|
|
17029
|
-
|
|
17030
|
-
|
|
17051
|
+
}
|
|
17052
|
+
if (cfgStatus === "malformed") {
|
|
17053
|
+
return { ok: false, error: cfgError, configPath: cfgPath };
|
|
17054
|
+
}
|
|
17055
|
+
if (cfgStatus === "missing") {
|
|
17056
|
+
return { ok: false, error: `vcs.json is missing: ${cfgPath}`, configPath: cfgPath };
|
|
17057
|
+
}
|
|
17058
|
+
if (provider === null) {
|
|
17059
|
+
return {
|
|
17060
|
+
ok: false,
|
|
17061
|
+
error: `no vcs provider configured (set vcs.provider in ${cfgPath}, match a workspace entry, or run inside a checkout with a recognized origin remote)`,
|
|
17062
|
+
configPath: cfgPath
|
|
17031
17063
|
};
|
|
17032
|
-
|
|
17033
|
-
|
|
17034
|
-
|
|
17035
|
-
|
|
17064
|
+
}
|
|
17065
|
+
const prov = cfg[provider] ?? {};
|
|
17066
|
+
const wsTokenFile = typeof wsVcs.tokenFile === "string" && wsVcs.tokenFile.trim() !== "" ? wsVcs.tokenFile : null;
|
|
17067
|
+
const tokenFile = String(wsTokenFile ?? prov.tokenFile ?? path7.join(configDir(), `${provider}.token`));
|
|
17068
|
+
const tokenPath = path7.resolve(tokenFile);
|
|
17069
|
+
let tokenOk = false;
|
|
17070
|
+
if (fs5.existsSync(tokenPath)) {
|
|
17071
|
+
const token = fs5.readFileSync(tokenPath, "utf8").trim();
|
|
17072
|
+
const placeholder = !token || token === TOKEN_PLACEHOLDER || token.startsWith(TOKEN_PLACEHOLDER);
|
|
17073
|
+
tokenOk = !placeholder;
|
|
17074
|
+
}
|
|
17075
|
+
const out = {
|
|
17076
|
+
ok: true,
|
|
17077
|
+
configPath: path7.resolve(cfgPath),
|
|
17078
|
+
provider,
|
|
17079
|
+
defaultTargetBranch: defaultTarget,
|
|
17080
|
+
pr: cfg.pr ?? {},
|
|
17081
|
+
tokenPath,
|
|
17082
|
+
tokenPresent: fs5.existsSync(tokenPath),
|
|
17083
|
+
tokenReady: tokenOk,
|
|
17084
|
+
workspace_name: ws?.name ?? null,
|
|
17085
|
+
link_issues: linkIssues,
|
|
17086
|
+
youtrack_base_url: youtrackBaseUrl,
|
|
17087
|
+
issues_provider: issuesProvider,
|
|
17088
|
+
link_on_pr: linkOnPr
|
|
17089
|
+
};
|
|
17090
|
+
if (provider === "gitlab") {
|
|
17091
|
+
out.gitlab = {
|
|
17092
|
+
host: prov.host ?? "gitlab.com",
|
|
17093
|
+
apiUrl: prov.apiUrl ?? "https://gitlab.com/api/v4"
|
|
17094
|
+
};
|
|
17095
|
+
} else if (provider === "github") {
|
|
17096
|
+
out.github = { host: prov.host ?? "github.com" };
|
|
17097
|
+
}
|
|
17098
|
+
if (mode === "summary")
|
|
17099
|
+
delete out.tokenPath;
|
|
17100
|
+
return out;
|
|
17101
|
+
}
|
|
17102
|
+
function mergedPrStyle(limit = 6, cwd) {
|
|
17103
|
+
const cfg = vcsConfig("load", cwd);
|
|
17104
|
+
if (!cfg.ok || !cfg.tokenReady)
|
|
17105
|
+
return { ok: false, error: "vcs not configured" };
|
|
17106
|
+
const provider = cfg.provider;
|
|
17107
|
+
const token = fs5.readFileSync(cfg.tokenPath, "utf8").trim();
|
|
17108
|
+
const examples = [];
|
|
17109
|
+
const descInfo = (desc, caseInsensitiveNotes = false) => ({
|
|
17110
|
+
hasNotesSection: caseInsensitiveNotes ? /##\s*notes/i.test(desc) : /##\s*Notes/.test(desc),
|
|
17111
|
+
sections: desc.split(`
|
|
17112
|
+
`).filter((l) => l.startsWith("## ")).map((l) => l.trim()),
|
|
17113
|
+
descriptionPreview: desc.slice(0, 600)
|
|
17114
|
+
});
|
|
17115
|
+
if (provider === "gitlab") {
|
|
17116
|
+
const remote = spawnSync("git", ["remote", "get-url", "origin"], { cwd, encoding: "utf8" });
|
|
17117
|
+
if (remote.status !== 0)
|
|
17118
|
+
return { ok: false, error: "no origin remote" };
|
|
17119
|
+
const url = (remote.stdout ?? "").trim();
|
|
17120
|
+
const m = /gitlab\.com[:/](.+?)(?:\.git)?$/.exec(url);
|
|
17121
|
+
if (!m)
|
|
17122
|
+
return { ok: false, error: "not a gitlab.com origin" };
|
|
17123
|
+
const project = m[1];
|
|
17124
|
+
const env = { ...process.env, GITLAB_TOKEN: token };
|
|
17125
|
+
const run = (args) => spawnSync("glab", ["api", ...args], { cwd, encoding: "utf8", env });
|
|
17126
|
+
let r = run([
|
|
17127
|
+
`projects/${project.replaceAll("/", "%2F")}/merge_requests?state=merged&per_page=${limit}&order_by=updated_at&sort=desc`
|
|
17128
|
+
]);
|
|
17129
|
+
if (r.status !== 0) {
|
|
17130
|
+
r = run([`merge_requests?state=merged&per_page=${limit}&order_by=updated_at&sort=desc`]);
|
|
17131
|
+
}
|
|
17132
|
+
if (r.status !== 0)
|
|
17133
|
+
return { ok: false, error: "could not list merge requests" };
|
|
17134
|
+
for (const mr of JSON.parse(r.stdout ?? "[]")) {
|
|
17135
|
+
const desc = String(mr.description ?? "").trim();
|
|
17136
|
+
examples.push({
|
|
17137
|
+
title: mr.title,
|
|
17138
|
+
url: mr.web_url,
|
|
17139
|
+
squash: mr.squash,
|
|
17140
|
+
...descInfo(desc, true)
|
|
17036
17141
|
});
|
|
17037
17142
|
}
|
|
17038
|
-
|
|
17039
|
-
|
|
17040
|
-
|
|
17041
|
-
|
|
17042
|
-
|
|
17043
|
-
|
|
17044
|
-
|
|
17045
|
-
if (outcomeResult)
|
|
17046
|
-
return outcomeResult;
|
|
17047
|
-
return success(changed.data.revision, input.workspaceRevision, entry);
|
|
17048
|
-
}
|
|
17049
|
-
function reconcileAction(input) {
|
|
17050
|
-
if (!input.store || !input.taskRevision || !input.workspaceRevision || !input.evidenceDigest)
|
|
17051
|
-
return failure("invalid_input", "action reconciliation preconditions are required");
|
|
17052
|
-
const authority = takeAuthority(input.authority, { owner: input.authorityOwner, store: input.store, root: input.store.root }, input.authorityCaller);
|
|
17053
|
-
if (!authority || authority.kind !== "reconciliation")
|
|
17054
|
-
return failure("permission_denied", "native reconciliation authority is not verified");
|
|
17055
|
-
if (authority.taskId !== input.taskId || authority.decisionId !== input.decisionId || !sameRef(authority.actionRef, input.actionRef) || authority.taskRevision !== input.taskRevision || authority.workspaceRevision !== input.workspaceRevision || authority.outcome !== input.outcome || authority.evidenceDigest !== input.evidenceDigest || authority.step !== input.step)
|
|
17056
|
-
return failure("permission_denied", "native reconciliation authority is not bound");
|
|
17057
|
-
const changed = input.store.mutateTask(input.taskId, input.taskRevision, (current, mutation) => {
|
|
17058
|
-
const workspace = input.store.readWorkspace();
|
|
17059
|
-
if (!workspace.ok)
|
|
17060
|
-
return workspace;
|
|
17061
|
-
if (!workspace.data)
|
|
17062
|
-
return failure("not_found", "workspace not found");
|
|
17063
|
-
if (authority.workspaceId !== workspace.data.id)
|
|
17064
|
-
return failure("permission_denied", "native reconciliation workspace does not match");
|
|
17065
|
-
if (workspace.data.revision !== input.workspaceRevision)
|
|
17066
|
-
return failure("revision_conflict", "workspace revision does not match");
|
|
17067
|
-
const entry = current.decisions.find((candidate) => candidate.id === input.decisionId);
|
|
17068
|
-
if (!entry || entry.data.purpose !== "action")
|
|
17069
|
-
return failure("not_found", "decision not found");
|
|
17070
|
-
if (!authorityDecisionMatches(authority, entry.data) || !provenanceMatches(entry.provenance, authority.provenance))
|
|
17071
|
-
return failure("permission_denied", "native reconciliation caller does not match approval");
|
|
17072
|
-
if (entry.data.response !== "approved" || entry.data.revoked !== null || entry.data.digest !== decisionDigest(entry.data) || entry.data.binding.taskId !== current.id || entry.data.binding.workspaceId !== current.workspaceId || entry.data.binding.workspaceId !== workspace.data.id)
|
|
17073
|
-
return failure("permission_denied", "reconciliation decision binding is invalid");
|
|
17074
|
-
if (entry.data.consumption?.state !== "uncertain")
|
|
17075
|
-
return failure("external_outcome_unknown", "only an uncertain action can be reconciled");
|
|
17076
|
-
if (!sameRef(entry.data.consumption.actionRef, input.actionRef))
|
|
17077
|
-
return failure("permission_denied", "reconciliation reference does not match reservation");
|
|
17078
|
-
const stored = current.actionProgress?.find((progress) => progress.decisionId === input.decisionId);
|
|
17079
|
-
const workflow = stored ? { steps: [...stored.steps], completed: [...stored.completedSteps] } : { steps: [], completed: [] };
|
|
17080
|
-
const currentStep = input.step ?? workflow.steps[workflow.completed.length];
|
|
17081
|
-
if (workflow.steps.length && (!currentStep || currentStep !== workflow.steps[workflow.completed.length]))
|
|
17082
|
-
return failure("permission_denied", "reconciliation step is outside the approved workflow");
|
|
17083
|
-
if (input.outcome === "not_started") {
|
|
17084
|
-
return success(mutation.revision, null, {
|
|
17085
|
-
...current,
|
|
17086
|
-
decisions: current.decisions.map((candidate) => candidate.id === input.decisionId ? { ...candidate, data: { ...candidate.data, consumption: null } } : candidate)
|
|
17087
|
-
});
|
|
17143
|
+
} else if (provider === "github") {
|
|
17144
|
+
const r = spawnSync("gh", ["pr", "list", "--state", "merged", "--limit", String(limit), "--json", "title,url,body"], { cwd, encoding: "utf8", env: { ...process.env, GH_TOKEN: token } });
|
|
17145
|
+
if (r.status !== 0)
|
|
17146
|
+
return { ok: false, error: "could not list pull requests" };
|
|
17147
|
+
for (const pr of JSON.parse(r.stdout ?? "[]")) {
|
|
17148
|
+
const desc = String(pr.body ?? "").trim();
|
|
17149
|
+
examples.push({ title: pr.title, url: pr.url, ...descInfo(desc) });
|
|
17088
17150
|
}
|
|
17089
|
-
|
|
17090
|
-
workflow.completed.push(currentStep);
|
|
17091
|
-
const complete = !workflow.steps.length || workflow.completed.length >= workflow.steps.length;
|
|
17092
|
-
const nextDecision = {
|
|
17093
|
-
...entry.data,
|
|
17094
|
-
consumption: complete ? { ...entry.data.consumption, state: "consumed", at: mutation.now } : null
|
|
17095
|
-
};
|
|
17096
|
-
const actionProgress = workflow.steps.length ? [
|
|
17097
|
-
...(current.actionProgress ?? []).filter((progress) => progress.decisionId !== input.decisionId),
|
|
17098
|
-
{
|
|
17099
|
-
decisionId: input.decisionId,
|
|
17100
|
-
steps: workflow.steps,
|
|
17101
|
-
completedSteps: workflow.completed
|
|
17102
|
-
}
|
|
17103
|
-
] : current.actionProgress ?? [];
|
|
17104
|
-
return success(mutation.revision, null, {
|
|
17105
|
-
...current,
|
|
17106
|
-
actionProgress,
|
|
17107
|
-
decisions: current.decisions.map((candidate) => candidate.id === input.decisionId ? { ...candidate, data: nextDecision } : candidate)
|
|
17108
|
-
});
|
|
17109
|
-
}, input.native?.now);
|
|
17110
|
-
if (!changed.ok)
|
|
17111
|
-
return changed;
|
|
17112
|
-
const entry = changed.data.decisions.find((candidate) => candidate.id === input.decisionId);
|
|
17113
|
-
return entry ? success(changed.data.revision, input.workspaceRevision, entry) : failure("recovery_required", "reconciled decision disappeared");
|
|
17114
|
-
}
|
|
17115
|
-
var bindingCovers = scopeCovers;
|
|
17116
|
-
var verifyDecisionContent = verifyContentRefs;
|
|
17117
|
-
|
|
17118
|
-
// packages/workit-core/src/core/task-evaluation.ts
|
|
17119
|
-
var digestBytes3 = (value) => createHash4("sha256").update(value).digest("hex");
|
|
17120
|
-
var inside = (root, candidate) => candidate === root || candidate.startsWith(`${root}${path6.sep}`);
|
|
17121
|
-
var relative = (root, candidate) => path6.relative(root, candidate).split(path6.sep).join("/") || ".";
|
|
17122
|
-
var canonicalPath = (value) => {
|
|
17123
|
-
if (typeof value !== "string" || !value)
|
|
17124
|
-
return null;
|
|
17125
|
-
if (value.includes("\\") || value.split("/").includes(".."))
|
|
17126
|
-
return null;
|
|
17127
|
-
const normalized = path6.posix.normalize(value);
|
|
17128
|
-
if (normalized === ".." || normalized.startsWith("../") || normalized.startsWith("/"))
|
|
17129
|
-
return null;
|
|
17130
|
-
return normalized === "." ? "." : normalized.replace(/\/$/, "");
|
|
17131
|
-
};
|
|
17132
|
-
var canonicalScope = (scope) => {
|
|
17133
|
-
const paths = scope.paths.length ? scope.paths : ["."];
|
|
17134
|
-
const normalizedPaths = paths.map(canonicalPath);
|
|
17135
|
-
const normalizedExclusions = scope.exclusions.map(canonicalPath);
|
|
17136
|
-
if (normalizedPaths.some((value) => value === null) || normalizedExclusions.some((value) => value === null))
|
|
17137
|
-
return null;
|
|
17151
|
+
}
|
|
17138
17152
|
return {
|
|
17139
|
-
|
|
17140
|
-
|
|
17141
|
-
|
|
17153
|
+
ok: true,
|
|
17154
|
+
provider,
|
|
17155
|
+
count: examples.length,
|
|
17156
|
+
styleHints: [
|
|
17157
|
+
"Prefer ## Summary bullets + ## Validation or ## Test plan only",
|
|
17158
|
+
"Do not add ## Notes with branch names, commit counts, or diff stats",
|
|
17159
|
+
"Do not paste commit log or diff stat into the body"
|
|
17160
|
+
],
|
|
17161
|
+
examples
|
|
17142
17162
|
};
|
|
17143
|
-
}
|
|
17144
|
-
|
|
17145
|
-
|
|
17146
|
-
|
|
17147
|
-
|
|
17148
|
-
|
|
17149
|
-
|
|
17150
|
-
|
|
17151
|
-
|
|
17152
|
-
|
|
17153
|
-
|
|
17154
|
-
|
|
17155
|
-
|
|
17156
|
-
|
|
17157
|
-
|
|
17158
|
-
|
|
17163
|
+
}
|
|
17164
|
+
|
|
17165
|
+
// packages/workit-core/src/core/branch.ts
|
|
17166
|
+
var resolveBranchPolicyFor = (workspaceRoot) => resolveBranchPolicy(readConfig(), resolveWorkspace(workspaceRoot));
|
|
17167
|
+
var verifyPushIdentity = (root, provider, account) => {
|
|
17168
|
+
if (!account || typeof account !== "string")
|
|
17169
|
+
return { ok: false, error: "push identity requires a configured area account" };
|
|
17170
|
+
const probe = provider === "gitlab" ? ["api", "user", "--jq", ".username"] : ["api", "user", "--jq", ".login"];
|
|
17171
|
+
const bin = provider === "gitlab" ? "glab" : "gh";
|
|
17172
|
+
let login;
|
|
17173
|
+
try {
|
|
17174
|
+
login = execFileSync2(bin, [...probe], {
|
|
17175
|
+
cwd: root,
|
|
17176
|
+
encoding: "utf8",
|
|
17177
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
17178
|
+
}).trim();
|
|
17179
|
+
} catch {
|
|
17180
|
+
return { ok: false, error: `${bin} identity could not be resolved` };
|
|
17159
17181
|
}
|
|
17160
|
-
|
|
17161
|
-
};
|
|
17162
|
-
var gitPaths = (root) => {
|
|
17163
|
-
const result = spawnSync("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
|
|
17164
|
-
cwd: root,
|
|
17165
|
-
encoding: "buffer"
|
|
17166
|
-
});
|
|
17167
|
-
if (result.status !== 0) {
|
|
17168
|
-
const stderr = result.stderr?.toString("utf8") ?? "";
|
|
17182
|
+
if (!login || login.toLowerCase() !== account.toLowerCase())
|
|
17169
17183
|
return {
|
|
17170
|
-
|
|
17171
|
-
|
|
17172
|
-
git: /not a git repository/i.test(stderr) ? false : true
|
|
17184
|
+
ok: false,
|
|
17185
|
+
error: `push identity ${login || "(unknown)"} does not match area account ${account}`
|
|
17173
17186
|
};
|
|
17174
|
-
}
|
|
17175
|
-
const paths = result.stdout ? result.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path6.sep).join("/")) : [];
|
|
17176
|
-
const stagedDeleted = spawnSync("git", ["diff", "--cached", "--name-only", "--diff-filter=D", "-z"], { cwd: root, encoding: "buffer" });
|
|
17177
|
-
if (stagedDeleted.status !== 0) {
|
|
17178
|
-
const stderr = stagedDeleted.stderr?.toString("utf8") ?? "";
|
|
17179
|
-
if (!/not a git repository/i.test(stderr))
|
|
17180
|
-
return { paths, uncertain: true, git: true };
|
|
17181
|
-
} else if (stagedDeleted.stdout) {
|
|
17182
|
-
paths.push(...stagedDeleted.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path6.sep).join("/")));
|
|
17183
|
-
}
|
|
17184
|
-
return { paths: [...new Set(paths)], uncertain: false, git: true };
|
|
17187
|
+
return { ok: true };
|
|
17185
17188
|
};
|
|
17186
|
-
|
|
17187
|
-
|
|
17189
|
+
|
|
17190
|
+
// packages/workit-core/src/core/auto-approval.ts
|
|
17191
|
+
var AUTO_CLASSES = ["branch", "commit", "push", "pr", "merge"];
|
|
17192
|
+
var OPERATION_CLASS = {
|
|
17193
|
+
"git.branch_setup": "branch",
|
|
17194
|
+
"git.commit": "commit",
|
|
17195
|
+
"git.push": "push",
|
|
17196
|
+
"hosting.pull_request": "pr",
|
|
17197
|
+
"hosting.merge": "merge"
|
|
17198
|
+
};
|
|
17199
|
+
var operationAutoClass = (operation) => typeof operation === "string" ? OPERATION_CLASS[operation] ?? null : null;
|
|
17200
|
+
var digestOf = (configDirPath) => {
|
|
17188
17201
|
try {
|
|
17189
|
-
|
|
17202
|
+
const raw = readFileSync4(`${configDirPath}/workspaces.json`, "utf8");
|
|
17203
|
+
return createHash3("sha256").update(raw).digest("hex");
|
|
17190
17204
|
} catch {
|
|
17191
|
-
return
|
|
17205
|
+
return null;
|
|
17192
17206
|
}
|
|
17193
|
-
|
|
17194
|
-
|
|
17195
|
-
|
|
17196
|
-
|
|
17197
|
-
|
|
17198
|
-
|
|
17199
|
-
|
|
17200
|
-
|
|
17201
|
-
|
|
17202
|
-
|
|
17203
|
-
continue;
|
|
17204
|
-
}
|
|
17205
|
-
const item = relative(root, target);
|
|
17206
|
-
output.add(item);
|
|
17207
|
-
if (stat.isDirectory() && !stat.isSymbolicLink())
|
|
17208
|
-
uncertain = walk(root, target, output) || uncertain;
|
|
17207
|
+
};
|
|
17208
|
+
var normalizeClasses = (value) => {
|
|
17209
|
+
if (value === true)
|
|
17210
|
+
return [...AUTO_CLASSES];
|
|
17211
|
+
if (!Array.isArray(value))
|
|
17212
|
+
return [];
|
|
17213
|
+
const seen = new Set;
|
|
17214
|
+
for (const item of value) {
|
|
17215
|
+
if (typeof item === "string" && AUTO_CLASSES.includes(item))
|
|
17216
|
+
seen.add(item);
|
|
17209
17217
|
}
|
|
17210
|
-
return
|
|
17218
|
+
return [...seen];
|
|
17211
17219
|
};
|
|
17212
|
-
var
|
|
17213
|
-
|
|
17214
|
-
|
|
17215
|
-
|
|
17216
|
-
|
|
17217
|
-
|
|
17218
|
-
let ancestor = target;
|
|
17219
|
-
while (!fs6.existsSync(ancestor) && ancestor !== root)
|
|
17220
|
-
ancestor = path6.dirname(ancestor);
|
|
17221
|
-
try {
|
|
17222
|
-
if (!inside(root, fs6.realpathSync(ancestor)))
|
|
17223
|
-
return failure("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` }] });
|
|
17224
|
-
} catch {
|
|
17225
|
-
return failure("invalid_input", `candidate scope cannot be inspected: ${item}`);
|
|
17226
|
-
}
|
|
17227
|
-
roots.push(target);
|
|
17220
|
+
var resolveAutoApproval = (root, dir = configDir()) => {
|
|
17221
|
+
let match;
|
|
17222
|
+
try {
|
|
17223
|
+
match = resolveWorkspaceFrom(root, dir);
|
|
17224
|
+
} catch {
|
|
17225
|
+
return { status: "off" };
|
|
17228
17226
|
}
|
|
17229
|
-
|
|
17227
|
+
if (!match)
|
|
17228
|
+
return { status: "off" };
|
|
17229
|
+
const classes = normalizeClasses(match.autoApprove);
|
|
17230
|
+
if (classes.length === 0)
|
|
17231
|
+
return { status: "off" };
|
|
17232
|
+
return {
|
|
17233
|
+
status: "on",
|
|
17234
|
+
workspace: match.name,
|
|
17235
|
+
classes,
|
|
17236
|
+
account: typeof match.vcs?.account === "string" && match.vcs.account ? match.vcs.account : null,
|
|
17237
|
+
configDigest: digestOf(dir)
|
|
17238
|
+
};
|
|
17230
17239
|
};
|
|
17231
|
-
|
|
17232
|
-
|
|
17240
|
+
var isStandingReceipt = (value) => typeof value === "object" && value !== null && value.kind === "standing";
|
|
17241
|
+
var standingApprovalLive = (root, receipt, cls, dir) => {
|
|
17242
|
+
if (!isStandingReceipt(receipt))
|
|
17243
|
+
return false;
|
|
17244
|
+
if (typeof cls !== "string" || !AUTO_CLASSES.includes(cls))
|
|
17245
|
+
return false;
|
|
17246
|
+
if (receipt.class !== cls)
|
|
17247
|
+
return false;
|
|
17248
|
+
const resolved = resolveAutoApproval(root, dir);
|
|
17249
|
+
if (resolved.status !== "on")
|
|
17250
|
+
return false;
|
|
17251
|
+
if (resolved.workspace !== receipt.workspace)
|
|
17252
|
+
return false;
|
|
17253
|
+
return resolved.classes.includes(cls);
|
|
17254
|
+
};
|
|
17255
|
+
var standingReceiptFor = (root, workspace, cls, dir) => {
|
|
17256
|
+
const resolved = resolveAutoApproval(root, dir);
|
|
17257
|
+
return {
|
|
17258
|
+
kind: "standing",
|
|
17259
|
+
workspace,
|
|
17260
|
+
class: cls,
|
|
17261
|
+
configDigest: resolved.status === "on" && resolved.workspace === workspace ? resolved.configDigest : null
|
|
17262
|
+
};
|
|
17263
|
+
};
|
|
17264
|
+
var verifyStandingApproval = (root, task, caller, binding) => {
|
|
17265
|
+
if (task.status !== "active")
|
|
17266
|
+
return failure("invalid_transition", "only active tasks can authorize actions");
|
|
17267
|
+
const session = task.intent?.provenance?.session;
|
|
17268
|
+
if (!session || session.kind !== "host" || session.host !== caller.host || session.handle !== caller.actor)
|
|
17269
|
+
return failure("permission_denied", "standing approval requires the lead session");
|
|
17270
|
+
const standing = binding.standing;
|
|
17271
|
+
if (!standing || typeof standing.workspace !== "string" || typeof standing.class !== "string")
|
|
17272
|
+
return failure("invalid_input", "standing approval needs a workspace and class");
|
|
17273
|
+
let operation;
|
|
17233
17274
|
try {
|
|
17234
|
-
|
|
17275
|
+
operation = typeof binding.approvedContent === "string" ? JSON.parse(binding.approvedContent).operation : null;
|
|
17235
17276
|
} catch {
|
|
17236
|
-
|
|
17277
|
+
operation = null;
|
|
17278
|
+
}
|
|
17279
|
+
const cls = operationAutoClass(operation);
|
|
17280
|
+
if (!cls || cls !== standing.class)
|
|
17281
|
+
return failure("permission_denied", "standing approval does not cover this operation");
|
|
17282
|
+
const resolved = resolveAutoApproval(root);
|
|
17283
|
+
if (resolved.status !== "on" || resolved.workspace !== standing.workspace || !resolved.classes.includes(cls))
|
|
17284
|
+
return failure("permission_denied", "standing auto-approval is not live for this operation");
|
|
17285
|
+
if (cls === "push") {
|
|
17286
|
+
const workspace = resolveWorkspaceFrom(root, configDir());
|
|
17287
|
+
const provider = workspace?.vcs?.provider;
|
|
17288
|
+
const account = resolved.account;
|
|
17289
|
+
if (!provider || !account)
|
|
17290
|
+
return failure("permission_denied", "auto-push requires a workspace provider and area account");
|
|
17291
|
+
const identity = verifyPushIdentity(root, provider, account);
|
|
17292
|
+
if (!identity.ok)
|
|
17293
|
+
return failure("permission_denied", identity.error);
|
|
17237
17294
|
}
|
|
17238
|
-
|
|
17239
|
-
|
|
17240
|
-
|
|
17241
|
-
|
|
17242
|
-
|
|
17243
|
-
|
|
17244
|
-
|
|
17245
|
-
|
|
17246
|
-
|
|
17247
|
-
|
|
17248
|
-
|
|
17249
|
-
|
|
17250
|
-
|
|
17251
|
-
|
|
17252
|
-
|
|
17253
|
-
|
|
17254
|
-
|
|
17255
|
-
|
|
17256
|
-
|
|
17257
|
-
|
|
17258
|
-
|
|
17295
|
+
return success(null, null, {
|
|
17296
|
+
kind: "host_observed",
|
|
17297
|
+
host: caller.host,
|
|
17298
|
+
session: { kind: "host", host: caller.host, handle: caller.actor },
|
|
17299
|
+
workerId: null,
|
|
17300
|
+
receipts: [standingReceiptFor(root, standing.workspace, cls)]
|
|
17301
|
+
});
|
|
17302
|
+
};
|
|
17303
|
+
|
|
17304
|
+
// packages/workit-core/src/core/authority.ts
|
|
17305
|
+
var verifiedAuthorities = new WeakMap;
|
|
17306
|
+
var trustedAuthority = (kind, provenance, expected, binding, caller) => {
|
|
17307
|
+
const token = {};
|
|
17308
|
+
verifiedAuthorities.set(token, { kind, provenance, ...expected, ...binding, caller });
|
|
17309
|
+
return token;
|
|
17310
|
+
};
|
|
17311
|
+
var chainStepKey = (step) => step.kind === "commit" ? step.message : step.kind === "branch" ? `branch:${step.target}` : "pr";
|
|
17312
|
+
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
17313
|
+
var normalizeChainSteps = (steps) => {
|
|
17314
|
+
if (!Array.isArray(steps) || steps.length === 0 || steps.length > 32)
|
|
17315
|
+
return null;
|
|
17316
|
+
const normalized = [];
|
|
17317
|
+
for (const step of steps) {
|
|
17318
|
+
if (typeof step === "string") {
|
|
17319
|
+
if (!step.trim())
|
|
17320
|
+
return null;
|
|
17321
|
+
normalized.push({ kind: "commit", message: step });
|
|
17259
17322
|
continue;
|
|
17260
17323
|
}
|
|
17261
|
-
if (!
|
|
17262
|
-
|
|
17263
|
-
|
|
17264
|
-
|
|
17265
|
-
|
|
17266
|
-
|
|
17267
|
-
|
|
17268
|
-
|
|
17269
|
-
for (const item of [...names].sort()) {
|
|
17270
|
-
if (!scopeMatches(item, normalizedScope))
|
|
17324
|
+
if (!isRecord(step))
|
|
17325
|
+
return null;
|
|
17326
|
+
if (typeof step.branch === "string" && step.branch.trim() && Object.keys(step).length === 1) {
|
|
17327
|
+
normalized.push({ kind: "branch", target: step.branch });
|
|
17328
|
+
continue;
|
|
17329
|
+
}
|
|
17330
|
+
if (step.pr === true && Object.keys(step).length === 1) {
|
|
17331
|
+
normalized.push({ kind: "pr" });
|
|
17271
17332
|
continue;
|
|
17272
|
-
const target = path6.join(checkout, item);
|
|
17273
|
-
try {
|
|
17274
|
-
const stat = fs6.lstatSync(target);
|
|
17275
|
-
if (stat.isSymbolicLink()) {
|
|
17276
|
-
const link = fs6.readlinkSync(target);
|
|
17277
|
-
files.push({ path: item, kind: "symlink", digest: digestBytes3(link), executable: null });
|
|
17278
|
-
} else if (stat.isFile()) {
|
|
17279
|
-
files.push({
|
|
17280
|
-
path: item,
|
|
17281
|
-
kind: "file",
|
|
17282
|
-
digest: digestBytes3(fs6.readFileSync(target)),
|
|
17283
|
-
executable: (stat.mode & 73) !== 0
|
|
17284
|
-
});
|
|
17285
|
-
} else if (!stat.isDirectory()) {
|
|
17286
|
-
completeness = "uncertain";
|
|
17287
|
-
} else if (git.git && git.paths.includes(item)) {
|
|
17288
|
-
completeness = "uncertain";
|
|
17289
|
-
}
|
|
17290
|
-
} catch (error) {
|
|
17291
|
-
if (error?.code === "ENOENT") {
|
|
17292
|
-
files.push({ path: item, kind: "absent", digest: null, executable: null });
|
|
17293
|
-
} else
|
|
17294
|
-
completeness = "uncertain";
|
|
17295
17333
|
}
|
|
17334
|
+
return null;
|
|
17296
17335
|
}
|
|
17297
|
-
const
|
|
17298
|
-
|
|
17299
|
-
|
|
17300
|
-
|
|
17301
|
-
const namesByEnvironment = supplied.map(({ name }) => name);
|
|
17302
|
-
if (new Set(namesByEnvironment).size !== namesByEnvironment.length || namesByEnvironment.some((name) => typeof name !== "string" || !name))
|
|
17303
|
-
return failure("invalid_input", "candidate environment names must be unique and non-empty");
|
|
17304
|
-
const values = supplied.sort((left, right) => compareCodeUnits2(left.name, right.name)).map(({ name, value }) => ({ name, value, refs: [] }));
|
|
17305
|
-
if (values.some(({ value }) => value === null))
|
|
17306
|
-
completeness = "uncertain";
|
|
17307
|
-
const headResult = spawnSync("git", ["rev-parse", "HEAD"], { cwd: checkout, encoding: "utf8" });
|
|
17308
|
-
if (headResult.status !== 0 && !/not a git repository/i.test(headResult.stderr ?? ""))
|
|
17309
|
-
completeness = "uncertain";
|
|
17310
|
-
const head = headResult.status === 0 ? headResult.stdout.trim() : null;
|
|
17311
|
-
const partial = {
|
|
17312
|
-
id: "0".repeat(64),
|
|
17313
|
-
scope: normalizedScope,
|
|
17314
|
-
completeness,
|
|
17315
|
-
files,
|
|
17316
|
-
environment: values,
|
|
17317
|
-
head
|
|
17318
|
-
};
|
|
17319
|
-
const candidate = { ...partial, id: candidateDigest(partial) };
|
|
17320
|
-
const parsed = candidateSchema.safeParse(candidate);
|
|
17321
|
-
return parsed.success ? success(null, null, parsed.data) : failure("invalid_input", "captured candidate is invalid");
|
|
17322
|
-
}
|
|
17323
|
-
var fileMap = (candidate) => new Map(candidate.files.map((file) => [file.path, file]));
|
|
17324
|
-
var changedPaths = (before, after) => {
|
|
17325
|
-
const paths = new Set([...before.files, ...after.files].map((file) => file.path));
|
|
17326
|
-
const left = fileMap(before);
|
|
17327
|
-
const right = fileMap(after);
|
|
17328
|
-
const changed = [...paths].filter((item) => JSON.stringify(left.get(item) ?? null) !== JSON.stringify(right.get(item) ?? null));
|
|
17329
|
-
if (before.head !== after.head)
|
|
17330
|
-
changed.push(".");
|
|
17331
|
-
if (JSON.stringify(before.environment) !== JSON.stringify(after.environment))
|
|
17332
|
-
changed.push(".");
|
|
17333
|
-
return changed;
|
|
17336
|
+
const keys = normalized.map(chainStepKey);
|
|
17337
|
+
if (new Set(keys).size !== keys.length)
|
|
17338
|
+
return null;
|
|
17339
|
+
return normalized;
|
|
17334
17340
|
};
|
|
17335
|
-
var
|
|
17336
|
-
|
|
17341
|
+
var scopeEqual = (left, right) => canonicalJson(left) === canonicalJson(right);
|
|
17342
|
+
var parseSteps = (content) => {
|
|
17343
|
+
let value;
|
|
17344
|
+
try {
|
|
17345
|
+
value = JSON.parse(content);
|
|
17346
|
+
} catch {
|
|
17347
|
+
return [];
|
|
17348
|
+
}
|
|
17349
|
+
if (typeof value !== "object" || value === null || !("steps" in value))
|
|
17350
|
+
return [];
|
|
17351
|
+
const steps = value.steps;
|
|
17352
|
+
if (!Array.isArray(steps))
|
|
17337
17353
|
return null;
|
|
17338
|
-
|
|
17354
|
+
const normalized = normalizeChainSteps(steps);
|
|
17355
|
+
return normalized;
|
|
17339
17356
|
};
|
|
17340
|
-
var
|
|
17341
|
-
const
|
|
17342
|
-
return
|
|
17357
|
+
var decisionMatches = (entry, purpose, binding) => {
|
|
17358
|
+
const decision = entry.data;
|
|
17359
|
+
return binding.taskId === decision.binding.taskId && binding.workspaceId === decision.binding.workspaceId && decision.purpose === purpose && decision.response === "approved" && decision.revoked === null && (purpose !== "action" || decision.consumption === null) && decision.digest === decisionDigest(decision) && scopeEqual(decision.binding.scope, binding.scope) && canonicalJson(decision.binding) === canonicalJson(binding);
|
|
17343
17360
|
};
|
|
17344
|
-
var
|
|
17345
|
-
|
|
17346
|
-
|
|
17347
|
-
|
|
17348
|
-
|
|
17349
|
-
|
|
17350
|
-
return {
|
|
17351
|
-
evidenceId: entry.id,
|
|
17352
|
-
status: "failed",
|
|
17353
|
-
reason: "failed evidence remains historical"
|
|
17354
|
-
};
|
|
17355
|
-
if (evidence.kind === "check" || evidence.kind === "review") {
|
|
17356
|
-
if (!evidence.beforeCandidateId || !evidence.candidateId)
|
|
17357
|
-
return {
|
|
17358
|
-
evidenceId: entry.id,
|
|
17359
|
-
status: "stale",
|
|
17360
|
-
reason: "check or review evidence lacks start and end candidates"
|
|
17361
|
-
};
|
|
17362
|
-
if (evidence.beforeCandidateId !== evidence.candidateId)
|
|
17363
|
-
return {
|
|
17364
|
-
evidenceId: entry.id,
|
|
17365
|
-
status: "stale",
|
|
17366
|
-
reason: "candidate changed during the check or review"
|
|
17367
|
-
};
|
|
17368
|
-
}
|
|
17369
|
-
if (!evidence.candidateId || !current)
|
|
17370
|
-
return {
|
|
17371
|
-
evidenceId: entry.id,
|
|
17372
|
-
status: evidence.result,
|
|
17373
|
-
reason: "evidence has no candidate binding"
|
|
17374
|
-
};
|
|
17375
|
-
const observed = task.candidates.find((item) => item.id === evidence.candidateId) ?? (current.id === evidence.candidateId ? current : undefined);
|
|
17376
|
-
if (!observed || observed.completeness === "uncertain" || current.completeness === "uncertain")
|
|
17377
|
-
return { evidenceId: entry.id, status: "stale", reason: "candidate is missing or uncertain" };
|
|
17378
|
-
if (observed.id === current.id)
|
|
17379
|
-
return { evidenceId: entry.id, status: evidence.result, reason: "candidate is unchanged" };
|
|
17380
|
-
const scopes = evidenceScopes(task, evidence.requirementIds);
|
|
17381
|
-
const changed = changedPaths(observed, current);
|
|
17382
|
-
const environmentChanged = JSON.stringify(observed.environment) !== JSON.stringify(current.environment);
|
|
17383
|
-
if (!scopes.length || environmentChanged || changed.some((item) => scopes.some((scope) => pathRelevant(item, scope))))
|
|
17384
|
-
return { evidenceId: entry.id, status: "stale", reason: "relevant candidate state changed" };
|
|
17385
|
-
return {
|
|
17386
|
-
evidenceId: entry.id,
|
|
17387
|
-
status: evidence.result,
|
|
17388
|
-
reason: "candidate changed outside evidence scope"
|
|
17389
|
-
};
|
|
17390
|
-
});
|
|
17391
|
-
}
|
|
17392
|
-
var scopeCovers2 = scopeCovers;
|
|
17393
|
-
var checkPin = (assignment, beforeCandidateId, candidateId) => {
|
|
17394
|
-
const pin = assignment.candidateId ?? null;
|
|
17395
|
-
if (pin === null)
|
|
17396
|
-
return true;
|
|
17397
|
-
return (beforeCandidateId ?? null) === pin || (candidateId ?? null) === pin;
|
|
17361
|
+
var sameRef = (left, right) => canonicalJson(left) === canonicalJson(right);
|
|
17362
|
+
var validProvenance = (value, caller) => {
|
|
17363
|
+
if (!provenanceSchema.safeParse(value).success)
|
|
17364
|
+
return false;
|
|
17365
|
+
const provenance = value;
|
|
17366
|
+
return provenance.kind === "host_observed" && provenance.host === caller.host && provenance.session?.kind === "host" && provenance.session.host === caller.host && provenance.session.handle === caller.actor && provenance.receipts.some((receipt) => receipt.kind === "host" && receipt.host === caller.host);
|
|
17398
17367
|
};
|
|
17399
|
-
var
|
|
17400
|
-
|
|
17401
|
-
|
|
17402
|
-
|
|
17403
|
-
|
|
17404
|
-
|
|
17405
|
-
|
|
17368
|
+
var verifyNativeDecision = (verifier, input, binding) => {
|
|
17369
|
+
if (!verifier)
|
|
17370
|
+
return failure("permission_denied", "native authority verifier is unavailable");
|
|
17371
|
+
const result = verifier.verifyDecision(input);
|
|
17372
|
+
if (!result.ok || !validProvenance(result.data, input.caller))
|
|
17373
|
+
return failure("permission_denied", "native decision observation was not attested");
|
|
17374
|
+
return success(null, null, trustedAuthority("decision", result.data, {
|
|
17375
|
+
taskId: input.expected.taskId,
|
|
17376
|
+
workspaceId: input.expected.workspaceId,
|
|
17377
|
+
purpose: input.expected.purpose,
|
|
17378
|
+
response: input.expected.response,
|
|
17379
|
+
binding: input.expected.binding,
|
|
17380
|
+
bindingBytes: input.expected.bindingBytes,
|
|
17381
|
+
digest: input.expected.digest,
|
|
17382
|
+
requirementIds: [...input.expected.requirementIds]
|
|
17383
|
+
}, binding, input.caller));
|
|
17406
17384
|
};
|
|
17407
|
-
var
|
|
17408
|
-
|
|
17409
|
-
|
|
17410
|
-
|
|
17411
|
-
if (
|
|
17412
|
-
return
|
|
17413
|
-
|
|
17414
|
-
|
|
17415
|
-
|
|
17416
|
-
|
|
17417
|
-
|
|
17418
|
-
|
|
17419
|
-
|
|
17420
|
-
|
|
17421
|
-
|
|
17385
|
+
var verifyNativeAction = (verifier, input, binding) => {
|
|
17386
|
+
if (!verifier)
|
|
17387
|
+
return failure("permission_denied", "native authority verifier is unavailable");
|
|
17388
|
+
const result = verifier.verifyAction(input);
|
|
17389
|
+
if (!result.ok || !validProvenance(result.data, input.caller))
|
|
17390
|
+
return failure("permission_denied", "native action observation was not attested");
|
|
17391
|
+
return success(null, null, trustedAuthority("action", result.data, {
|
|
17392
|
+
taskId: input.expected.taskId,
|
|
17393
|
+
workspaceId: input.expected.workspaceId,
|
|
17394
|
+
decisionId: input.expected.decisionId,
|
|
17395
|
+
actionRef: input.expected.actionRef,
|
|
17396
|
+
taskRevision: input.expected.taskRevision,
|
|
17397
|
+
workspaceRevision: input.expected.workspaceRevision,
|
|
17398
|
+
outcome: input.expected.outcome,
|
|
17399
|
+
purpose: input.expected.decision.purpose,
|
|
17400
|
+
response: input.expected.decision.response,
|
|
17401
|
+
binding: input.expected.decision.binding,
|
|
17402
|
+
digest: input.expected.decision.digest,
|
|
17403
|
+
requirementIds: [...input.expected.decision.requirementIds]
|
|
17404
|
+
}, binding, input.caller));
|
|
17422
17405
|
};
|
|
17423
|
-
var
|
|
17424
|
-
if (
|
|
17425
|
-
return "
|
|
17426
|
-
|
|
17427
|
-
|
|
17428
|
-
|
|
17429
|
-
|
|
17430
|
-
|
|
17431
|
-
|
|
17432
|
-
|
|
17433
|
-
|
|
17434
|
-
|
|
17435
|
-
|
|
17436
|
-
|
|
17406
|
+
var verifyNativeReconciliation = (verifier, input, binding) => {
|
|
17407
|
+
if (!verifier?.verifyReconciliation)
|
|
17408
|
+
return failure("permission_denied", "native reconciliation authority is unavailable");
|
|
17409
|
+
const result = verifier.verifyReconciliation(input);
|
|
17410
|
+
if (!result.ok || !validProvenance(result.data, input.caller))
|
|
17411
|
+
return failure("permission_denied", "native reconciliation observation was not attested");
|
|
17412
|
+
return success(null, null, trustedAuthority("reconciliation", result.data, {
|
|
17413
|
+
taskId: input.expected.taskId,
|
|
17414
|
+
workspaceId: input.expected.workspaceId,
|
|
17415
|
+
decisionId: input.expected.decisionId,
|
|
17416
|
+
actionRef: input.expected.actionRef,
|
|
17417
|
+
taskRevision: input.expected.taskRevision,
|
|
17418
|
+
workspaceRevision: input.expected.workspaceRevision,
|
|
17419
|
+
outcome: input.expected.outcome,
|
|
17420
|
+
evidenceDigest: input.expected.evidenceDigest,
|
|
17421
|
+
step: input.expected.step,
|
|
17422
|
+
purpose: input.expected.decision.purpose,
|
|
17423
|
+
response: input.expected.decision.response,
|
|
17424
|
+
binding: input.expected.decision.binding,
|
|
17425
|
+
digest: input.expected.decision.digest,
|
|
17426
|
+
requirementIds: [...input.expected.decision.requirementIds]
|
|
17427
|
+
}, binding, input.caller));
|
|
17437
17428
|
};
|
|
17438
|
-
|
|
17439
|
-
const
|
|
17440
|
-
|
|
17441
|
-
|
|
17442
|
-
|
|
17443
|
-
|
|
17444
|
-
|
|
17445
|
-
|
|
17446
|
-
|
|
17447
|
-
|
|
17448
|
-
|
|
17449
|
-
|
|
17450
|
-
|
|
17451
|
-
|
|
17452
|
-
|
|
17453
|
-
|
|
17454
|
-
|
|
17455
|
-
|
|
17456
|
-
|
|
17457
|
-
|
|
17458
|
-
|
|
17459
|
-
const sameEvidenceSession = related.some(({ entry: other }) => other.id !== entry.id && other.data.kind === "review" && other.data.candidateId !== null && other.data.candidateId === entry.data.candidateId && sameSession(reviewSession, other.provenance.session));
|
|
17460
|
-
return entry.data.kind === "review" && reviewSession !== null && sameSession(reviewSession, entry.provenance.session) && (requirement.ruleId === "self-review" || !sameImplementation && !sameEvidenceSession);
|
|
17461
|
-
});
|
|
17462
|
-
if (passed.length) {
|
|
17463
|
-
const currentId = candidate?.id ?? null;
|
|
17464
|
-
const withRed = requirement.dimension === "testing" ? passed.filter(({ entry }) => related.some((other) => other.entry.data.kind === "check" && other.evaluation.status === "failed" && other.entry.data.requirementIds.includes(requirement.id) && other.entry.recordedAt <= entry.recordedAt) || related.some((baseline) => baseline.entry.id !== entry.id && baseline.entry.data.kind === "check" && baseline.entry.data.result === "passed" && baseline.entry.data.candidateId !== null && currentId !== null && baseline.entry.data.candidateId !== currentId)) : passed;
|
|
17465
|
-
if (withRed.length)
|
|
17466
|
-
return {
|
|
17467
|
-
requirementId: requirement.id,
|
|
17468
|
-
status: "satisfied",
|
|
17469
|
-
evidenceIds: withRed.map(({ entry }) => entry.id),
|
|
17470
|
-
decisionIds: [],
|
|
17471
|
-
reason: "fresh applicable evidence passed"
|
|
17472
|
-
};
|
|
17473
|
-
if (requirement.dimension === "testing")
|
|
17474
|
-
return {
|
|
17475
|
-
requirementId: requirement.id,
|
|
17476
|
-
status: "unsatisfied",
|
|
17477
|
-
evidenceIds: [],
|
|
17478
|
-
decisionIds: [],
|
|
17479
|
-
reason: "testing requirement has GREEN evidence but no preceding RED"
|
|
17480
|
-
};
|
|
17481
|
-
}
|
|
17482
|
-
if (requirement.dimension === "delegation") {
|
|
17483
|
-
const dot = (scope) => scope.paths.length > 0 ? scope : { ...scope, paths: ["."] };
|
|
17484
|
-
const reconciled = task.workers.some((worker) => worker.data.report?.outcome === "completed" && (worker.data.assignment.requirementIds.includes(requirement.id) || scopeCovers2(dot(requirement.scope), dot(worker.data.assignment.scope))));
|
|
17485
|
-
if (reconciled)
|
|
17486
|
-
return {
|
|
17487
|
-
requirementId: requirement.id,
|
|
17488
|
-
status: "satisfied",
|
|
17489
|
-
evidenceIds: [],
|
|
17490
|
-
decisionIds: [],
|
|
17491
|
-
reason: "bounded helper completed and its result is recorded"
|
|
17492
|
-
};
|
|
17493
|
-
}
|
|
17494
|
-
const decisions = requirement.dimension === "decisions" ? applicableRequirementDecision(task, workspace, requirement, checkoutRoot) : [];
|
|
17495
|
-
if (decisions.length)
|
|
17496
|
-
return {
|
|
17497
|
-
requirementId: requirement.id,
|
|
17498
|
-
status: "satisfied",
|
|
17499
|
-
evidenceIds: [],
|
|
17500
|
-
decisionIds: decisions.map((decision) => decision.id),
|
|
17501
|
-
reason: "an applicable approved or stated decision satisfies the requirement"
|
|
17502
|
-
};
|
|
17503
|
-
const limitations = requirement.acceptanceAllowed ? applicableDecision2(task, workspace, requirement, checkoutRoot) : [];
|
|
17504
|
-
if (limitations.length)
|
|
17505
|
-
return {
|
|
17506
|
-
requirementId: requirement.id,
|
|
17507
|
-
status: "accepted_limitation",
|
|
17508
|
-
evidenceIds: [],
|
|
17509
|
-
decisionIds: limitations.map((decision) => task.decisions.find((entry) => entry.data === decision).id),
|
|
17510
|
-
reason: "an applicable approved limitation permits the missing evidence"
|
|
17511
|
-
};
|
|
17512
|
-
const unavailable = capabilities.some((capability) => capability.assurance === "unavailable" && (capability.name === requirement.dimension || capability.surface === requirement.dimension));
|
|
17513
|
-
if (!unavailable && related.length > 0 && !passed.length)
|
|
17514
|
-
return {
|
|
17515
|
-
requirementId: requirement.id,
|
|
17516
|
-
status: "unsatisfied",
|
|
17517
|
-
evidenceIds: related.map(({ entry }) => entry.id),
|
|
17518
|
-
decisionIds: [],
|
|
17519
|
-
reason: `no passing evidence (needs kind:${expectedKinds(requirement.dimension, requirement.ruleId)})`
|
|
17520
|
-
};
|
|
17521
|
-
return {
|
|
17522
|
-
requirementId: requirement.id,
|
|
17523
|
-
status: unavailable ? "unavailable" : "unsatisfied",
|
|
17524
|
-
evidenceIds: related.map(({ entry }) => entry.id),
|
|
17525
|
-
decisionIds: [],
|
|
17526
|
-
reason: unavailable ? "required capability is unavailable" : "no fresh passing evidence"
|
|
17527
|
-
};
|
|
17528
|
-
});
|
|
17429
|
+
var takeAuthority = (authority, binding, caller) => {
|
|
17430
|
+
const record = verifiedAuthorities.get(authority);
|
|
17431
|
+
verifiedAuthorities.delete(authority);
|
|
17432
|
+
if (!record)
|
|
17433
|
+
return null;
|
|
17434
|
+
if (record.owner !== binding.owner || record.store !== binding.store || record.root !== binding.root || record.root !== record.store.root || canonicalJson(record.caller) !== canonicalJson(caller))
|
|
17435
|
+
return null;
|
|
17436
|
+
return record;
|
|
17437
|
+
};
|
|
17438
|
+
var retireNativeAuthority = (authority) => {
|
|
17439
|
+
const record = verifiedAuthorities.get(authority);
|
|
17440
|
+
verifiedAuthorities.delete(authority);
|
|
17441
|
+
return record?.provenance ?? null;
|
|
17442
|
+
};
|
|
17443
|
+
var authorityMatches = (authority, expected) => authority.kind === "action" && authority.taskId === expected.taskId && authority.workspaceId === expected.workspaceId && authority.decisionId === expected.decisionId && sameRef(authority.actionRef, expected.actionRef) && authority.taskRevision === expected.taskRevision && authority.workspaceRevision === expected.workspaceRevision && authority.outcome === expected.outcome;
|
|
17444
|
+
var authorityDecisionMatches = (authority, decision) => authority.purpose === decision.purpose && authority.response === decision.response && authority.digest === decision.digest && canonicalJson(authority.binding) === canonicalJson(decision.binding) && canonicalJson(authority.requirementIds) === canonicalJson(decision.requirementIds);
|
|
17445
|
+
var provenanceMatches = (left, right) => left.kind === "host_observed" && right.kind === "host_observed" && left.host === right.host && canonicalJson(left.session) === canonicalJson(right.session);
|
|
17446
|
+
function applicableDecision(task, purpose, binding, checkoutRoot) {
|
|
17447
|
+
if (binding.taskId !== task.id || binding.workspaceId !== task.workspaceId)
|
|
17448
|
+
return [];
|
|
17449
|
+
return task.decisions.filter((entry) => entry.provenance.kind !== "imported" && decisionMatches(entry, purpose, binding) && (checkoutRoot ? verifyDecisionContentAtRoot(checkoutRoot, entry.data.binding).ok : entry.data.binding.contentRefs.every((reference) => reference.kind !== "file"))).map((entry) => entry.data);
|
|
17529
17450
|
}
|
|
17530
|
-
|
|
17531
|
-
|
|
17532
|
-
|
|
17533
|
-
|
|
17534
|
-
|
|
17535
|
-
|
|
17536
|
-
|
|
17537
|
-
|
|
17538
|
-
|
|
17539
|
-
|
|
17540
|
-
|
|
17541
|
-
|
|
17542
|
-
|
|
17543
|
-
|
|
17544
|
-
|
|
17545
|
-
|
|
17546
|
-
|
|
17547
|
-
|
|
17548
|
-
|
|
17549
|
-
|
|
17550
|
-
|
|
17451
|
+
var digestBytes2 = (bytes) => createHash4("sha256").update(bytes).digest("hex");
|
|
17452
|
+
var verifyDecisionContentAtRoot = (checkoutRoot, binding) => {
|
|
17453
|
+
let root;
|
|
17454
|
+
try {
|
|
17455
|
+
root = fs6.realpathSync(checkoutRoot);
|
|
17456
|
+
} catch {
|
|
17457
|
+
return failure("permission_denied", "checkout root is unavailable");
|
|
17458
|
+
}
|
|
17459
|
+
for (const reference of binding.contentRefs) {
|
|
17460
|
+
if (reference.kind !== "file")
|
|
17461
|
+
continue;
|
|
17462
|
+
if (!reference.digest)
|
|
17463
|
+
return failure("invalid_input", "document references require a byte digest");
|
|
17464
|
+
const target = path8.resolve(root, reference.path);
|
|
17465
|
+
if (target !== root && !target.startsWith(`${root}${path8.sep}`))
|
|
17466
|
+
return failure("invalid_input", "document reference escapes checkout; cite it as an external file:// reference instead", {
|
|
17467
|
+
fields: [
|
|
17468
|
+
{ path: "contentRefs.path", reason: `${reference.path} is outside the checkout` }
|
|
17469
|
+
]
|
|
17470
|
+
});
|
|
17471
|
+
try {
|
|
17472
|
+
const relative = path8.relative(root, target);
|
|
17473
|
+
let current = root;
|
|
17474
|
+
for (const segment of relative.split(path8.sep)) {
|
|
17475
|
+
if (!segment)
|
|
17476
|
+
continue;
|
|
17477
|
+
current = path8.join(current, segment);
|
|
17478
|
+
const stat = fs6.lstatSync(current);
|
|
17479
|
+
const real = fs6.realpathSync(current);
|
|
17480
|
+
if (real !== root && !real.startsWith(`${root}${path8.sep}`))
|
|
17481
|
+
return failure("invalid_input", "document reference escapes checkout; cite it as an external file:// reference instead", {
|
|
17482
|
+
fields: [
|
|
17483
|
+
{
|
|
17484
|
+
path: "contentRefs.path",
|
|
17485
|
+
reason: `${reference.path} resolves outside the checkout`
|
|
17486
|
+
}
|
|
17487
|
+
]
|
|
17551
17488
|
});
|
|
17552
|
-
|
|
17553
|
-
|
|
17554
|
-
|
|
17489
|
+
if (stat.isSymbolicLink())
|
|
17490
|
+
return failure("invalid_input", "document reference uses a symlink");
|
|
17491
|
+
if (current === target && !stat.isFile())
|
|
17492
|
+
return failure("invalid_input", "document reference is not a regular file");
|
|
17493
|
+
if (current !== target && !stat.isDirectory())
|
|
17494
|
+
return failure("invalid_input", "document reference ancestor is not a directory");
|
|
17555
17495
|
}
|
|
17556
|
-
if (
|
|
17557
|
-
|
|
17558
|
-
|
|
17559
|
-
|
|
17560
|
-
|
|
17561
|
-
|
|
17562
|
-
|
|
17563
|
-
|
|
17496
|
+
if (digestBytes2(fs6.readFileSync(target)) !== reference.digest)
|
|
17497
|
+
return failure("permission_denied", "approved document bytes have changed");
|
|
17498
|
+
} catch {
|
|
17499
|
+
return failure("invalid_input", "approved document is unavailable");
|
|
17500
|
+
}
|
|
17501
|
+
}
|
|
17502
|
+
return success(null, null, null);
|
|
17503
|
+
};
|
|
17504
|
+
var verifyContentRefs = (store, binding) => verifyDecisionContentAtRoot(store.root, binding);
|
|
17505
|
+
var storedDecisionApplicable = (store, task, purpose, binding) => task.decisions.filter((entry) => entry.provenance.kind !== "imported" && decisionMatches(entry, purpose, binding) && verifyDecisionContentAtRoot(store.root, entry.data.binding).ok);
|
|
17506
|
+
var validateAction = (store, task, workspaceId, input, authority) => {
|
|
17507
|
+
if (task.status !== "active")
|
|
17508
|
+
return failure("invalid_transition", "only active tasks can authorize actions");
|
|
17509
|
+
if (!refSchema.safeParse(input.actionRef).success)
|
|
17510
|
+
return failure("invalid_input", "action reference is invalid");
|
|
17511
|
+
const entry = task.decisions.find((candidate) => candidate.id === input.decisionId);
|
|
17512
|
+
if (!entry)
|
|
17513
|
+
return failure("not_found", "decision not found");
|
|
17514
|
+
const decision = entry.data;
|
|
17515
|
+
if (decision.purpose !== "action")
|
|
17516
|
+
return failure("permission_denied", "decision is not an action approval");
|
|
17517
|
+
if (!authorityMatches(authority, {
|
|
17518
|
+
taskId: task.id,
|
|
17519
|
+
workspaceId,
|
|
17520
|
+
decisionId: input.decisionId,
|
|
17521
|
+
actionRef: input.actionRef,
|
|
17522
|
+
taskRevision: input.expectedRevision,
|
|
17523
|
+
workspaceRevision: input.expectedWorkspaceRevision,
|
|
17524
|
+
outcome: "reserve"
|
|
17525
|
+
}))
|
|
17526
|
+
return failure("permission_denied", "native reservation authority is not bound");
|
|
17527
|
+
if (!authorityDecisionMatches(authority, decision))
|
|
17528
|
+
return failure("permission_denied", "native reservation authority does not match decision");
|
|
17529
|
+
if (decision.response !== "approved")
|
|
17530
|
+
return failure("permission_denied", "decision was rejected");
|
|
17531
|
+
if (entry.provenance.kind !== "host_observed" || entry.provenance.receipts.length === 0 || !provenanceMatches(entry.provenance, authority.provenance))
|
|
17532
|
+
return failure("permission_denied", "action approval lacks native receipt assurance");
|
|
17533
|
+
const receipts = entry.provenance.receipts;
|
|
17534
|
+
const standing = receipts.filter((receipt) => typeof receipt === "object" && receipt !== null && receipt.kind === "standing");
|
|
17535
|
+
const questionBacked = receipts.some((receipt) => typeof receipt === "object" && receipt !== null && receipt.kind === "host");
|
|
17536
|
+
if (!questionBacked && standing.length > 0) {
|
|
17537
|
+
let operation;
|
|
17538
|
+
try {
|
|
17539
|
+
operation = JSON.parse(decision.binding.approvedContent).operation;
|
|
17540
|
+
} catch {
|
|
17541
|
+
operation = null;
|
|
17542
|
+
}
|
|
17543
|
+
const cls = operationAutoClass(operation);
|
|
17544
|
+
if (!cls || !standing.some((receipt) => standingApprovalLive(store.root, receipt, cls)))
|
|
17545
|
+
return failure("permission_denied", "standing auto-approval is not live for this operation");
|
|
17546
|
+
}
|
|
17547
|
+
if (decision.revoked)
|
|
17548
|
+
return failure("permission_denied", "decision is revoked");
|
|
17549
|
+
if (decision.digest !== decisionDigest(decision))
|
|
17550
|
+
return failure("permission_denied", "decision binding is invalid");
|
|
17551
|
+
if (decision.binding.taskId !== task.id || decision.binding.workspaceId !== workspaceId)
|
|
17552
|
+
return failure("permission_denied", "decision task or workspace binding is invalid");
|
|
17553
|
+
if (input.binding && canonicalJson(input.binding) !== canonicalJson(decision.binding))
|
|
17554
|
+
return failure("permission_denied", "action binding does not match the approved decision");
|
|
17555
|
+
const content = verifyContentRefs(store, decision.binding);
|
|
17556
|
+
if (!content.ok)
|
|
17557
|
+
return content;
|
|
17558
|
+
const requestedSteps = parseSteps(decision.binding.approvedContent);
|
|
17559
|
+
if (requestedSteps === null)
|
|
17560
|
+
return failure("invalid_input", "bounded action steps must be a unique non-empty array");
|
|
17561
|
+
if (input.steps && canonicalJson(input.steps) !== canonicalJson(requestedSteps.map(chainStepKey)))
|
|
17562
|
+
return failure("permission_denied", "bounded action steps do not match approved content");
|
|
17563
|
+
const stored = task.actionProgress?.find((item) => item.decisionId === input.decisionId);
|
|
17564
|
+
const workflow = stored ? { steps: stored.steps, completed: stored.completedSteps } : { steps: requestedSteps.map(chainStepKey), completed: [] };
|
|
17565
|
+
if (workflow.steps.length && requestedSteps.length && canonicalJson(workflow.steps) !== canonicalJson(requestedSteps.map(chainStepKey)))
|
|
17566
|
+
return failure("permission_denied", "bounded action scope changed");
|
|
17567
|
+
return success(null, null, { entry, workflow });
|
|
17568
|
+
};
|
|
17569
|
+
function reserveAction(input) {
|
|
17570
|
+
if (!input.store || !input.expectedRevision || !input.expectedWorkspaceRevision)
|
|
17571
|
+
return failure("invalid_input", "action reservation preconditions are required");
|
|
17572
|
+
const authority = takeAuthority(input.authority, {
|
|
17573
|
+
owner: input.authorityOwner,
|
|
17574
|
+
store: input.store,
|
|
17575
|
+
root: input.store.root
|
|
17576
|
+
}, input.authorityCaller);
|
|
17577
|
+
if (!authority)
|
|
17578
|
+
return failure("permission_denied", "native reservation authority is not verified");
|
|
17579
|
+
const changed = input.store.mutateTask(input.taskId, input.expectedRevision, (current, mutation) => {
|
|
17580
|
+
const workspace = input.store.readWorkspace();
|
|
17581
|
+
if (!workspace.ok)
|
|
17582
|
+
return workspace;
|
|
17583
|
+
if (!workspace.data)
|
|
17584
|
+
return failure("not_found", "workspace not found");
|
|
17585
|
+
if (workspace.data.revision !== input.expectedWorkspaceRevision)
|
|
17586
|
+
return failure("revision_conflict", "workspace revision does not match", {
|
|
17587
|
+
expectedWorkspaceRevision: input.expectedWorkspaceRevision,
|
|
17588
|
+
actualWorkspaceRevision: workspace.data.revision
|
|
17589
|
+
});
|
|
17590
|
+
const valid = validateAction(input.store, current, workspace.data.id, input, authority);
|
|
17591
|
+
if (!valid.ok)
|
|
17592
|
+
return valid;
|
|
17593
|
+
const { entry, workflow } = valid.data;
|
|
17594
|
+
const existing = entry.data.consumption;
|
|
17595
|
+
if (existing?.state === "uncertain")
|
|
17596
|
+
return failure("external_outcome_unknown", "previous action outcome is unknown", {
|
|
17597
|
+
operation: "reserve_action",
|
|
17598
|
+
outcome: "unknown"
|
|
17599
|
+
});
|
|
17600
|
+
if (existing?.state === "consumed")
|
|
17601
|
+
return failure("permission_denied", "action approval is consumed");
|
|
17602
|
+
if (existing?.state === "reserved" && workflow.completed.length === 0)
|
|
17603
|
+
return failure("permission_denied", "action approval is already reserved");
|
|
17604
|
+
const requestedStep = input.step ?? workflow.steps[workflow.completed.length];
|
|
17605
|
+
if (workflow.steps.length && (!requestedStep || requestedStep !== workflow.steps[workflow.completed.length]))
|
|
17606
|
+
return failure("permission_denied", "action step is outside the approved remaining workflow");
|
|
17607
|
+
if (requestedStep && workflow.completed.includes(requestedStep))
|
|
17608
|
+
return failure("permission_denied", "action step was already completed");
|
|
17609
|
+
const nextDecision = {
|
|
17610
|
+
...entry.data,
|
|
17611
|
+
consumption: { state: "reserved", at: mutation.now, actionRef: input.actionRef }
|
|
17612
|
+
};
|
|
17613
|
+
const actionProgress = workflow.steps.length ? [
|
|
17614
|
+
...(current.actionProgress ?? []).filter((progress) => progress.decisionId !== input.decisionId),
|
|
17615
|
+
{
|
|
17616
|
+
decisionId: input.decisionId,
|
|
17617
|
+
steps: [...workflow.steps],
|
|
17618
|
+
completedSteps: [...workflow.completed]
|
|
17564
17619
|
}
|
|
17565
|
-
|
|
17566
|
-
|
|
17567
|
-
|
|
17568
|
-
|
|
17569
|
-
|
|
17570
|
-
const requirement = view.task.policy?.requirements.find((candidate) => candidate.id === item.requirementId);
|
|
17571
|
-
return !requirement || requirement.before === "close";
|
|
17572
|
-
});
|
|
17573
|
-
if (requestedOutcome !== "stopped" && blocking.length)
|
|
17574
|
-
return failure("requirements_unsatisfied", "applicable requirements are unsatisfied", {
|
|
17575
|
-
requirementIds: blocking.map((item) => item.requirementId),
|
|
17576
|
-
requirements: blocking.map((item) => {
|
|
17577
|
-
const requirement = view.task.policy?.requirements.find((candidate) => candidate.id === item.requirementId);
|
|
17578
|
-
return {
|
|
17579
|
-
requirementId: item.requirementId,
|
|
17580
|
-
ruleId: requirement?.ruleId ?? "unknown",
|
|
17581
|
-
reason: item.reason,
|
|
17582
|
-
satisfaction: requirement?.satisfaction ?? "",
|
|
17583
|
-
dependentAction: requirement?.dependentAction ?? null
|
|
17584
|
-
};
|
|
17585
|
-
})
|
|
17586
|
-
});
|
|
17587
|
-
const accepted = evaluations.filter((item) => item.status === "accepted_limitation");
|
|
17588
|
-
if (requestedOutcome === "verified" && accepted.length)
|
|
17589
|
-
return failure("requirements_unsatisfied", "accepted limitations cannot be reported as verified", {
|
|
17590
|
-
requirementIds: accepted.map((item) => item.requirementId)
|
|
17591
|
-
});
|
|
17592
|
-
const evidenceIds = [
|
|
17593
|
-
...new Set([
|
|
17594
|
-
...evaluations.flatMap((item) => item.evidenceIds),
|
|
17595
|
-
...view.task.findings.flatMap((item) => item.data.resolution?.evidenceIds ?? [])
|
|
17596
|
-
])
|
|
17597
|
-
];
|
|
17598
|
-
const decisionIds = [
|
|
17599
|
-
...new Set([
|
|
17600
|
-
...evaluations.flatMap((item) => item.decisionIds),
|
|
17601
|
-
...view.task.findings.flatMap((item) => item.data.resolution?.decisionIds ?? [])
|
|
17602
|
-
])
|
|
17603
|
-
];
|
|
17604
|
-
return success(null, null, {
|
|
17605
|
-
outcome: requestedOutcome === "stopped" ? "stopped" : accepted.length ? "accepted_limitations" : "verified",
|
|
17606
|
-
evidenceIds,
|
|
17607
|
-
decisionIds,
|
|
17608
|
-
requirementIds: evaluations.map((item) => item.requirementId)
|
|
17609
|
-
});
|
|
17610
|
-
}
|
|
17611
|
-
|
|
17612
|
-
// packages/workit-core/src/core/task-context.ts
|
|
17613
|
-
function reconcileResume(view, observations = []) {
|
|
17614
|
-
if (observations.length > 0)
|
|
17615
|
-
return failure("permission_denied", "worker observations require native host verification");
|
|
17616
|
-
const candidate = captureCandidate(view.workspace.root, view.task.intent.data.scope, []);
|
|
17617
|
-
if (!candidate.ok)
|
|
17618
|
-
return candidate;
|
|
17619
|
-
const staleEvidenceIds = evaluateEvidence(view.task, candidate.data).filter((entry) => entry.status === "stale").map((entry) => entry.evidenceId);
|
|
17620
|
-
const blockers = [...view.task.progress.blockers];
|
|
17621
|
-
if (view.task.workers.some((entry) => isUncertainWorker(entry.data.state)))
|
|
17622
|
-
blockers.push({
|
|
17623
|
-
reason: "worker state requires reconciliation",
|
|
17624
|
-
dependentAction: "resume",
|
|
17625
|
-
refs: []
|
|
17620
|
+
] : current.actionProgress ?? [];
|
|
17621
|
+
return success(mutation.revision, null, {
|
|
17622
|
+
...current,
|
|
17623
|
+
actionProgress,
|
|
17624
|
+
decisions: current.decisions.map((candidate) => candidate.id === input.decisionId ? { ...candidate, data: nextDecision } : candidate)
|
|
17626
17625
|
});
|
|
17627
|
-
|
|
17628
|
-
|
|
17629
|
-
|
|
17630
|
-
|
|
17631
|
-
|
|
17632
|
-
|
|
17626
|
+
}, input.native?.now);
|
|
17627
|
+
if (!changed.ok)
|
|
17628
|
+
return changed;
|
|
17629
|
+
const workspace = input.store.readWorkspace();
|
|
17630
|
+
if (!workspace.ok || !workspace.data)
|
|
17631
|
+
return failure("recovery_required", "workspace disappeared after reservation");
|
|
17632
|
+
const entry = changed.data.decisions.find((candidate) => candidate.id === input.decisionId);
|
|
17633
|
+
if (!entry)
|
|
17634
|
+
return failure("recovery_required", "reserved decision disappeared");
|
|
17635
|
+
const workflow = changed.data.actionProgress?.find((progress) => progress.decisionId === input.decisionId) ?? {
|
|
17636
|
+
steps: [],
|
|
17637
|
+
completedSteps: []
|
|
17638
|
+
};
|
|
17639
|
+
return success(changed.data.revision, workspace.data.revision, {
|
|
17640
|
+
taskId: input.taskId,
|
|
17641
|
+
decisionId: input.decisionId,
|
|
17642
|
+
actionRef: input.actionRef,
|
|
17643
|
+
taskRevision: changed.data.revision,
|
|
17644
|
+
workspaceRevision: workspace.data.revision,
|
|
17645
|
+
completedSteps: [...workflow.completedSteps],
|
|
17646
|
+
remainingSteps: workflow.steps.slice(workflow.completedSteps.length)
|
|
17633
17647
|
});
|
|
17634
17648
|
}
|
|
17635
|
-
|
|
17636
|
-
|
|
17637
|
-
|
|
17638
|
-
|
|
17639
|
-
|
|
17640
|
-
|
|
17641
|
-
|
|
17642
|
-
|
|
17643
|
-
|
|
17644
|
-
|
|
17645
|
-
|
|
17646
|
-
|
|
17647
|
-
|
|
17648
|
-
|
|
17649
|
-
|
|
17650
|
-
|
|
17651
|
-
|
|
17652
|
-
|
|
17653
|
-
|
|
17654
|
-
|
|
17655
|
-
|
|
17656
|
-
|
|
17649
|
+
function settleAction(input) {
|
|
17650
|
+
if (!input.store || !input.taskRevision || !input.workspaceRevision)
|
|
17651
|
+
return failure("invalid_input", "action settlement preconditions are required");
|
|
17652
|
+
const authority = takeAuthority(input.authority, {
|
|
17653
|
+
owner: input.authorityOwner,
|
|
17654
|
+
store: input.store,
|
|
17655
|
+
root: input.store.root
|
|
17656
|
+
}, input.authorityCaller);
|
|
17657
|
+
if (!authority)
|
|
17658
|
+
return failure("permission_denied", "native settlement authority is not verified");
|
|
17659
|
+
if (!refSchema.safeParse(input.actionRef).success)
|
|
17660
|
+
return failure("invalid_input", "action reference is invalid");
|
|
17661
|
+
if (!authorityMatches(authority, {
|
|
17662
|
+
taskId: input.taskId,
|
|
17663
|
+
workspaceId: authority.workspaceId,
|
|
17664
|
+
decisionId: input.decisionId,
|
|
17665
|
+
actionRef: input.actionRef,
|
|
17666
|
+
taskRevision: input.taskRevision,
|
|
17667
|
+
workspaceRevision: input.workspaceRevision,
|
|
17668
|
+
outcome: input.outcome
|
|
17669
|
+
}))
|
|
17670
|
+
return failure("permission_denied", "native settlement authority is not verified");
|
|
17671
|
+
let outcomeResult = null;
|
|
17672
|
+
const changed = input.store.mutateTask(input.taskId, input.taskRevision, (current, mutation) => {
|
|
17673
|
+
const workspace = input.store.readWorkspace();
|
|
17674
|
+
if (!workspace.ok)
|
|
17675
|
+
return workspace;
|
|
17676
|
+
if (!workspace.data)
|
|
17677
|
+
return failure("not_found", "workspace not found");
|
|
17678
|
+
if (!authorityMatches(authority, {
|
|
17679
|
+
taskId: input.taskId,
|
|
17680
|
+
workspaceId: workspace.data.id,
|
|
17681
|
+
decisionId: input.decisionId,
|
|
17682
|
+
actionRef: input.actionRef,
|
|
17683
|
+
taskRevision: input.taskRevision,
|
|
17684
|
+
workspaceRevision: input.workspaceRevision,
|
|
17685
|
+
outcome: input.outcome
|
|
17686
|
+
}))
|
|
17687
|
+
return failure("permission_denied", "native settlement authority does not match context");
|
|
17688
|
+
if (workspace.data.revision !== input.workspaceRevision)
|
|
17689
|
+
return failure("revision_conflict", "workspace revision does not match", {
|
|
17690
|
+
expectedWorkspaceRevision: input.workspaceRevision,
|
|
17691
|
+
actualWorkspaceRevision: workspace.data.revision
|
|
17692
|
+
});
|
|
17693
|
+
const entry = current.decisions.find((candidate) => candidate.id === input.decisionId);
|
|
17694
|
+
if (!entry)
|
|
17695
|
+
return failure("not_found", "decision not found");
|
|
17696
|
+
if (!authorityDecisionMatches(authority, entry.data))
|
|
17697
|
+
return failure("permission_denied", "native settlement authority does not match decision");
|
|
17698
|
+
if (!provenanceMatches(entry.provenance, authority.provenance))
|
|
17699
|
+
return failure("permission_denied", "native settlement caller does not match approval");
|
|
17700
|
+
if (entry.data.purpose !== "action" || entry.data.digest !== decisionDigest(entry.data))
|
|
17701
|
+
return failure("permission_denied", "decision binding is invalid");
|
|
17702
|
+
if (entry.data.consumption?.state === "uncertain")
|
|
17703
|
+
return failure("external_outcome_unknown", "previous action outcome is unknown", {
|
|
17704
|
+
operation: "settle_action",
|
|
17705
|
+
outcome: "unknown"
|
|
17706
|
+
});
|
|
17707
|
+
if (entry.data.consumption?.state !== "reserved")
|
|
17708
|
+
return failure("permission_denied", "action is not reserved");
|
|
17709
|
+
if (canonicalJson(entry.data.consumption.actionRef) !== canonicalJson(input.actionRef))
|
|
17710
|
+
return failure("permission_denied", "settlement reference does not match reservation");
|
|
17711
|
+
const stored = current.actionProgress?.find((progress) => progress.decisionId === input.decisionId);
|
|
17712
|
+
const workflow = stored ? { steps: stored.steps, completed: stored.completedSteps } : { steps: [], completed: [] };
|
|
17713
|
+
if (entry.data.binding.approvedContent && !stored && (parseSteps(entry.data.binding.approvedContent) ?? []).length)
|
|
17714
|
+
return failure("recovery_required", "bounded action progress is missing");
|
|
17715
|
+
const currentStep = input.step ?? workflow.steps[workflow.completed.length];
|
|
17716
|
+
if (workflow.steps.length && (!currentStep || currentStep !== workflow.steps[workflow.completed.length]))
|
|
17717
|
+
return failure("permission_denied", "settlement step is outside the approved workflow");
|
|
17718
|
+
if (input.outcome === "unknown") {
|
|
17719
|
+
outcomeResult = failure("external_outcome_unknown", "external action outcome is unknown", {
|
|
17720
|
+
operation: "settle_action",
|
|
17721
|
+
outcome: "unknown"
|
|
17722
|
+
});
|
|
17723
|
+
const uncertain = {
|
|
17724
|
+
...entry.data,
|
|
17725
|
+
consumption: { ...entry.data.consumption, state: "uncertain", at: mutation.now }
|
|
17726
|
+
};
|
|
17727
|
+
return success(mutation.revision, null, {
|
|
17728
|
+
...current,
|
|
17729
|
+
decisions: current.decisions.map((candidate) => candidate.id === input.decisionId ? { ...candidate, data: uncertain } : candidate)
|
|
17730
|
+
});
|
|
17731
|
+
}
|
|
17732
|
+
if (input.outcome === "not_started") {
|
|
17733
|
+
const released = { ...entry.data, consumption: null };
|
|
17734
|
+
return success(mutation.revision, null, {
|
|
17735
|
+
...current,
|
|
17736
|
+
decisions: current.decisions.map((candidate) => candidate.id === input.decisionId ? { ...candidate, data: released } : candidate)
|
|
17737
|
+
});
|
|
17738
|
+
}
|
|
17739
|
+
if (currentStep)
|
|
17740
|
+
workflow.completed.push(currentStep);
|
|
17741
|
+
const complete = !workflow.steps.length || workflow.completed.length >= workflow.steps.length;
|
|
17742
|
+
const settled = {
|
|
17743
|
+
...entry.data,
|
|
17744
|
+
consumption: complete ? { ...entry.data.consumption, state: "consumed", at: mutation.now } : null
|
|
17745
|
+
};
|
|
17746
|
+
const actionProgress = workflow.steps.length ? [
|
|
17747
|
+
...(current.actionProgress ?? []).filter((progress) => progress.decisionId !== input.decisionId),
|
|
17748
|
+
{
|
|
17749
|
+
decisionId: input.decisionId,
|
|
17750
|
+
steps: [...workflow.steps],
|
|
17751
|
+
completedSteps: [...workflow.completed]
|
|
17752
|
+
}
|
|
17753
|
+
] : current.actionProgress ?? [];
|
|
17754
|
+
const next = {
|
|
17755
|
+
...current,
|
|
17756
|
+
actionProgress,
|
|
17757
|
+
decisions: current.decisions.map((candidate) => candidate.id === input.decisionId ? { ...candidate, data: settled } : candidate)
|
|
17657
17758
|
};
|
|
17658
|
-
|
|
17659
|
-
|
|
17660
|
-
|
|
17661
|
-
|
|
17662
|
-
|
|
17663
|
-
const runHere = (args) => {
|
|
17664
|
-
const result = run(cwd, args);
|
|
17665
|
-
if (!failure && result.exitCode !== 0) {
|
|
17666
|
-
failure = { stderr: result.stderr, exitCode: result.exitCode };
|
|
17759
|
+
if (!complete) {
|
|
17760
|
+
outcomeResult = success(mutation.revision, input.workspaceRevision, {
|
|
17761
|
+
...entry,
|
|
17762
|
+
data: settled
|
|
17763
|
+
});
|
|
17667
17764
|
}
|
|
17668
|
-
return
|
|
17669
|
-
};
|
|
17670
|
-
|
|
17671
|
-
|
|
17672
|
-
const
|
|
17673
|
-
|
|
17674
|
-
|
|
17675
|
-
|
|
17676
|
-
|
|
17677
|
-
|
|
17678
|
-
|
|
17679
|
-
|
|
17680
|
-
|
|
17681
|
-
|
|
17682
|
-
|
|
17683
|
-
|
|
17684
|
-
|
|
17685
|
-
|
|
17765
|
+
return success(mutation.revision, null, next);
|
|
17766
|
+
}, input.native?.now);
|
|
17767
|
+
if (!changed.ok)
|
|
17768
|
+
return changed;
|
|
17769
|
+
const entry = changed.data.decisions.find((candidate) => candidate.id === input.decisionId);
|
|
17770
|
+
if (!entry)
|
|
17771
|
+
return failure("recovery_required", "settled decision disappeared");
|
|
17772
|
+
if (outcomeResult)
|
|
17773
|
+
return outcomeResult;
|
|
17774
|
+
return success(changed.data.revision, input.workspaceRevision, entry);
|
|
17775
|
+
}
|
|
17776
|
+
function reconcileAction(input) {
|
|
17777
|
+
if (!input.store || !input.taskRevision || !input.workspaceRevision || !input.evidenceDigest)
|
|
17778
|
+
return failure("invalid_input", "action reconciliation preconditions are required");
|
|
17779
|
+
const authority = takeAuthority(input.authority, { owner: input.authorityOwner, store: input.store, root: input.store.root }, input.authorityCaller);
|
|
17780
|
+
if (!authority || authority.kind !== "reconciliation")
|
|
17781
|
+
return failure("permission_denied", "native reconciliation authority is not verified");
|
|
17782
|
+
if (authority.taskId !== input.taskId || authority.decisionId !== input.decisionId || !sameRef(authority.actionRef, input.actionRef) || authority.taskRevision !== input.taskRevision || authority.workspaceRevision !== input.workspaceRevision || authority.outcome !== input.outcome || authority.evidenceDigest !== input.evidenceDigest || authority.step !== input.step)
|
|
17783
|
+
return failure("permission_denied", "native reconciliation authority is not bound");
|
|
17784
|
+
const changed = input.store.mutateTask(input.taskId, input.taskRevision, (current, mutation) => {
|
|
17785
|
+
const workspace = input.store.readWorkspace();
|
|
17786
|
+
if (!workspace.ok)
|
|
17787
|
+
return workspace;
|
|
17788
|
+
if (!workspace.data)
|
|
17789
|
+
return failure("not_found", "workspace not found");
|
|
17790
|
+
if (authority.workspaceId !== workspace.data.id)
|
|
17791
|
+
return failure("permission_denied", "native reconciliation workspace does not match");
|
|
17792
|
+
if (workspace.data.revision !== input.workspaceRevision)
|
|
17793
|
+
return failure("revision_conflict", "workspace revision does not match");
|
|
17794
|
+
const entry = current.decisions.find((candidate) => candidate.id === input.decisionId);
|
|
17795
|
+
if (!entry || entry.data.purpose !== "action")
|
|
17796
|
+
return failure("not_found", "decision not found");
|
|
17797
|
+
if (!authorityDecisionMatches(authority, entry.data) || !provenanceMatches(entry.provenance, authority.provenance))
|
|
17798
|
+
return failure("permission_denied", "native reconciliation caller does not match approval");
|
|
17799
|
+
if (entry.data.response !== "approved" || entry.data.revoked !== null || entry.data.digest !== decisionDigest(entry.data) || entry.data.binding.taskId !== current.id || entry.data.binding.workspaceId !== current.workspaceId || entry.data.binding.workspaceId !== workspace.data.id)
|
|
17800
|
+
return failure("permission_denied", "reconciliation decision binding is invalid");
|
|
17801
|
+
if (entry.data.consumption?.state !== "uncertain")
|
|
17802
|
+
return failure("external_outcome_unknown", "only an uncertain action can be reconciled");
|
|
17803
|
+
if (!sameRef(entry.data.consumption.actionRef, input.actionRef))
|
|
17804
|
+
return failure("permission_denied", "reconciliation reference does not match reservation");
|
|
17805
|
+
const stored = current.actionProgress?.find((progress) => progress.decisionId === input.decisionId);
|
|
17806
|
+
const workflow = stored ? { steps: [...stored.steps], completed: [...stored.completedSteps] } : { steps: [], completed: [] };
|
|
17807
|
+
const currentStep = input.step ?? workflow.steps[workflow.completed.length];
|
|
17808
|
+
if (workflow.steps.length && (!currentStep || currentStep !== workflow.steps[workflow.completed.length]))
|
|
17809
|
+
return failure("permission_denied", "reconciliation step is outside the approved workflow");
|
|
17810
|
+
if (input.outcome === "not_started") {
|
|
17811
|
+
return success(mutation.revision, null, {
|
|
17812
|
+
...current,
|
|
17813
|
+
decisions: current.decisions.map((candidate) => candidate.id === input.decisionId ? { ...candidate, data: { ...candidate.data, consumption: null } } : candidate)
|
|
17814
|
+
});
|
|
17686
17815
|
}
|
|
17687
|
-
|
|
17688
|
-
|
|
17689
|
-
|
|
17690
|
-
|
|
17691
|
-
|
|
17692
|
-
|
|
17693
|
-
|
|
17694
|
-
|
|
17695
|
-
|
|
17696
|
-
|
|
17697
|
-
|
|
17698
|
-
|
|
17699
|
-
|
|
17700
|
-
|
|
17701
|
-
|
|
17702
|
-
|
|
17703
|
-
|
|
17704
|
-
|
|
17705
|
-
|
|
17706
|
-
|
|
17707
|
-
};
|
|
17708
|
-
|
|
17709
|
-
|
|
17710
|
-
|
|
17711
|
-
|
|
17712
|
-
|
|
17713
|
-
|
|
17714
|
-
|
|
17715
|
-
readFileSync as readFileSync4,
|
|
17716
|
-
readdirSync as readdirSync3,
|
|
17717
|
-
writeFileSync as writeFileSync2
|
|
17718
|
-
} from "node:fs";
|
|
17719
|
-
import os from "node:os";
|
|
17720
|
-
import path7 from "node:path";
|
|
17721
|
-
|
|
17722
|
-
// packages/workit-core/src/core/boundary.ts
|
|
17723
|
-
import { statSync } from "node:fs";
|
|
17724
|
-
var EVENT = {
|
|
17725
|
-
initialization: "initialization",
|
|
17726
|
-
provenance: "provenance",
|
|
17727
|
-
assets: "assets",
|
|
17728
|
-
configurationSource: "configuration_source",
|
|
17729
|
-
hooks: "hooks",
|
|
17730
|
-
mcpConnection: "mcp_connection",
|
|
17731
|
-
migration: "migration",
|
|
17732
|
-
installSteps: "install_steps",
|
|
17733
|
-
uncaughtFailure: "uncaught_failure",
|
|
17734
|
-
toolsFailed: "tools_failed",
|
|
17735
|
-
doctor: "doctor"
|
|
17736
|
-
};
|
|
17737
|
-
var errorDetail = (err) => {
|
|
17738
|
-
if (err instanceof Error) {
|
|
17739
|
-
return { error_name: err.name, error: err.message };
|
|
17740
|
-
}
|
|
17741
|
-
return { error_name: "unknown", error: String(err) };
|
|
17742
|
-
};
|
|
17743
|
-
var markSourcesLoaded = (files, loadedAt = Date.now()) => ({
|
|
17744
|
-
loadedAt,
|
|
17745
|
-
files: [...files]
|
|
17746
|
-
});
|
|
17747
|
-
var changedSourcesSinceLoad = (marker) => {
|
|
17748
|
-
const changed = [];
|
|
17749
|
-
for (const file of marker.files) {
|
|
17750
|
-
try {
|
|
17751
|
-
if (statSync(file).mtimeMs > marker.loadedAt)
|
|
17752
|
-
changed.push(file);
|
|
17753
|
-
} catch {}
|
|
17754
|
-
}
|
|
17755
|
-
return changed;
|
|
17756
|
-
};
|
|
17816
|
+
if (currentStep)
|
|
17817
|
+
workflow.completed.push(currentStep);
|
|
17818
|
+
const complete = !workflow.steps.length || workflow.completed.length >= workflow.steps.length;
|
|
17819
|
+
const nextDecision = {
|
|
17820
|
+
...entry.data,
|
|
17821
|
+
consumption: complete ? { ...entry.data.consumption, state: "consumed", at: mutation.now } : null
|
|
17822
|
+
};
|
|
17823
|
+
const actionProgress = workflow.steps.length ? [
|
|
17824
|
+
...(current.actionProgress ?? []).filter((progress) => progress.decisionId !== input.decisionId),
|
|
17825
|
+
{
|
|
17826
|
+
decisionId: input.decisionId,
|
|
17827
|
+
steps: workflow.steps,
|
|
17828
|
+
completedSteps: workflow.completed
|
|
17829
|
+
}
|
|
17830
|
+
] : current.actionProgress ?? [];
|
|
17831
|
+
return success(mutation.revision, null, {
|
|
17832
|
+
...current,
|
|
17833
|
+
actionProgress,
|
|
17834
|
+
decisions: current.decisions.map((candidate) => candidate.id === input.decisionId ? { ...candidate, data: nextDecision } : candidate)
|
|
17835
|
+
});
|
|
17836
|
+
}, input.native?.now);
|
|
17837
|
+
if (!changed.ok)
|
|
17838
|
+
return changed;
|
|
17839
|
+
const entry = changed.data.decisions.find((candidate) => candidate.id === input.decisionId);
|
|
17840
|
+
return entry ? success(changed.data.revision, input.workspaceRevision, entry) : failure("recovery_required", "reconciled decision disappeared");
|
|
17841
|
+
}
|
|
17842
|
+
var bindingCovers = scopeCovers;
|
|
17843
|
+
var verifyDecisionContent = verifyContentRefs;
|
|
17757
17844
|
|
|
17758
|
-
// packages/workit-core/src/core/
|
|
17759
|
-
var
|
|
17760
|
-
var
|
|
17761
|
-
|
|
17762
|
-
|
|
17763
|
-
|
|
17764
|
-
|
|
17765
|
-
"
|
|
17766
|
-
|
|
17767
|
-
|
|
17768
|
-
|
|
17769
|
-
var mergePreset = (preset, input = {}, current = {
|
|
17770
|
-
branchPolicy: { preset, allowed: [], protected: [] }
|
|
17771
|
-
}) => {
|
|
17772
|
-
const defs = PRESETS[preset];
|
|
17773
|
-
return {
|
|
17774
|
-
preset,
|
|
17775
|
-
allowed: preset === "custom" ? input.allowed ?? current.branchPolicy.allowed : [...defs.allowed],
|
|
17776
|
-
protected: preset === "custom" ? input.protectedNames ?? current.branchPolicy.protected : [...defs.protected]
|
|
17777
|
-
};
|
|
17778
|
-
};
|
|
17779
|
-
var resolveConfigDir = () => process.env.WORKFLOW_TOOLKIT_CONFIG ?? process.env.WORKFLOW_TOOLKIT_CONFIG_DIR ?? path7.join(process.env.XDG_CONFIG_HOME || path7.join(os.homedir(), ".config"), "workit");
|
|
17780
|
-
var migratedDir = null;
|
|
17781
|
-
var migrationFailed = false;
|
|
17782
|
-
var ensureConfigDir = (dir = resolveConfigDir()) => {
|
|
17783
|
-
if (migratedDir === dir)
|
|
17784
|
-
return dir;
|
|
17785
|
-
if (process.env.WORKFLOW_TOOLKIT_CONFIG || process.env.WORKFLOW_TOOLKIT_CONFIG_DIR) {
|
|
17786
|
-
migratedDir = dir;
|
|
17787
|
-
return dir;
|
|
17788
|
-
}
|
|
17789
|
-
const legacy = path7.join(process.env.XDG_CONFIG_HOME || path7.join(os.homedir(), ".config"), "workflow-toolkit");
|
|
17790
|
-
if (!existsSync4(legacy)) {
|
|
17791
|
-
migratedDir = dir;
|
|
17792
|
-
return dir;
|
|
17793
|
-
}
|
|
17794
|
-
if (!migrationFailed && existsSync4(dir)) {
|
|
17795
|
-
migratedDir = dir;
|
|
17796
|
-
return dir;
|
|
17797
|
-
}
|
|
17798
|
-
migrationFailed = false;
|
|
17799
|
-
mkdirSync2(dir, { recursive: true });
|
|
17800
|
-
diagnosticLogger?.info(EVENT.migration, { from: legacy, to: dir });
|
|
17801
|
-
for (const entry of readdirSync3(legacy, { withFileTypes: true })) {
|
|
17802
|
-
const src = path7.join(legacy, entry.name);
|
|
17803
|
-
const dest = path7.join(dir, entry.name);
|
|
17804
|
-
if (existsSync4(dest))
|
|
17805
|
-
continue;
|
|
17806
|
-
try {
|
|
17807
|
-
if (entry.isDirectory())
|
|
17808
|
-
cpSync(src, dest, { recursive: true });
|
|
17809
|
-
else if (entry.isFile())
|
|
17810
|
-
copyFileSync(src, dest);
|
|
17811
|
-
} catch (err) {
|
|
17812
|
-
migrationFailed = true;
|
|
17813
|
-
diagnosticLogger?.warn(EVENT.migration, { from: src, ok: false, ...errorDetail(err) });
|
|
17814
|
-
console.warn(`[workit] config migration: a file could not be copied to ${dir}; it will be retried on the next run`);
|
|
17815
|
-
}
|
|
17816
|
-
}
|
|
17817
|
-
if (!migrationFailed)
|
|
17818
|
-
migratedDir = dir;
|
|
17819
|
-
return dir;
|
|
17820
|
-
};
|
|
17821
|
-
var configDir = () => ensureConfigDir();
|
|
17822
|
-
var LOCALE_RE = /^[a-z]{2,3}(-(?:[A-Z]{2}|[0-9]{3}))?$/;
|
|
17823
|
-
var DEFAULTS = {
|
|
17824
|
-
locale: "en",
|
|
17825
|
-
localeOptions: ["en", "es-CL", "es-MX", "es-AR", "pt-BR"],
|
|
17826
|
-
timezone: "America/Santiago",
|
|
17827
|
-
branchPolicy: {
|
|
17828
|
-
preset: "gitflow",
|
|
17829
|
-
allowed: [...PRESETS.gitflow.allowed],
|
|
17830
|
-
protected: [...PRESETS.gitflow.protected]
|
|
17831
|
-
},
|
|
17832
|
-
commitPolicy: { preset: "conventional" }
|
|
17833
|
-
};
|
|
17834
|
-
var readSafe = (p) => {
|
|
17835
|
-
try {
|
|
17836
|
-
return readFileSync4(p, "utf8");
|
|
17837
|
-
} catch {
|
|
17845
|
+
// packages/workit-core/src/core/task-evaluation.ts
|
|
17846
|
+
var digestBytes3 = (value) => createHash5("sha256").update(value).digest("hex");
|
|
17847
|
+
var inside = (root, candidate) => candidate === root || candidate.startsWith(`${root}${path9.sep}`);
|
|
17848
|
+
var relative = (root, candidate) => path9.relative(root, candidate).split(path9.sep).join("/") || ".";
|
|
17849
|
+
var canonicalPath = (value) => {
|
|
17850
|
+
if (typeof value !== "string" || !value)
|
|
17851
|
+
return null;
|
|
17852
|
+
if (value.includes("\\") || value.split("/").includes(".."))
|
|
17853
|
+
return null;
|
|
17854
|
+
const normalized = path9.posix.normalize(value);
|
|
17855
|
+
if (normalized === ".." || normalized.startsWith("../") || normalized.startsWith("/"))
|
|
17838
17856
|
return null;
|
|
17839
|
-
|
|
17857
|
+
return normalized === "." ? "." : normalized.replace(/\/$/, "");
|
|
17840
17858
|
};
|
|
17841
|
-
var
|
|
17842
|
-
|
|
17843
|
-
|
|
17844
|
-
|
|
17845
|
-
|
|
17846
|
-
|
|
17847
|
-
parsed = JSON.parse(raw);
|
|
17848
|
-
} catch {
|
|
17849
|
-
return { status: "malformed", path: file, error: `${file} is not valid JSON` };
|
|
17850
|
-
}
|
|
17851
|
-
if (!isConfigObject(parsed)) {
|
|
17852
|
-
return { status: "malformed", path: file, error: `${file} is not a JSON object` };
|
|
17853
|
-
}
|
|
17854
|
-
const input = parsed;
|
|
17855
|
-
const locale = LOCALE_RE.test(String(input.locale ?? "")) ? input.locale : DEFAULTS.locale;
|
|
17856
|
-
const preset = Object.hasOwn(PRESETS, input.branchPolicy?.preset) ? input.branchPolicy?.preset : "gitflow";
|
|
17857
|
-
const commitPreset = COMMIT_PRESETS.includes(String(input.commitPolicy?.preset ?? "")) ? input.commitPolicy?.preset : "conventional";
|
|
17859
|
+
var canonicalScope = (scope) => {
|
|
17860
|
+
const paths = scope.paths.length ? scope.paths : ["."];
|
|
17861
|
+
const normalizedPaths = paths.map(canonicalPath);
|
|
17862
|
+
const normalizedExclusions = scope.exclusions.map(canonicalPath);
|
|
17863
|
+
if (normalizedPaths.some((value) => value === null) || normalizedExclusions.some((value) => value === null))
|
|
17864
|
+
return null;
|
|
17858
17865
|
return {
|
|
17859
|
-
|
|
17860
|
-
|
|
17861
|
-
|
|
17862
|
-
locale,
|
|
17863
|
-
localeOptions: Array.isArray(input.localeOptions) ? input.localeOptions : DEFAULTS.localeOptions,
|
|
17864
|
-
timezone: input.timezone ?? DEFAULTS.timezone,
|
|
17865
|
-
branchPolicy: mergePreset(preset, {
|
|
17866
|
-
allowed: Array.isArray(input.branchPolicy?.allowed) ? input.branchPolicy.allowed : undefined,
|
|
17867
|
-
protectedNames: Array.isArray(input.branchPolicy?.protected) ? input.branchPolicy.protected : undefined
|
|
17868
|
-
}, DEFAULTS),
|
|
17869
|
-
commitPolicy: {
|
|
17870
|
-
preset: commitPreset,
|
|
17871
|
-
...typeof input.commitPolicy?.pattern === "string" ? { pattern: input.commitPolicy.pattern } : {}
|
|
17872
|
-
}
|
|
17873
|
-
}
|
|
17866
|
+
description: scope.description,
|
|
17867
|
+
paths: [...new Set(normalizedPaths)],
|
|
17868
|
+
exclusions: [...new Set(normalizedExclusions)]
|
|
17874
17869
|
};
|
|
17875
17870
|
};
|
|
17876
|
-
var
|
|
17877
|
-
|
|
17878
|
-
|
|
17879
|
-
|
|
17880
|
-
|
|
17881
|
-
const
|
|
17882
|
-
|
|
17883
|
-
throw new Error(result.error);
|
|
17884
|
-
return result.config ?? DEFAULTS;
|
|
17871
|
+
var excluded = (value, exclusions) => exclusions.some((item) => item === value || item !== "." && value.startsWith(`${item}/`));
|
|
17872
|
+
var scopeMatches = (value, scope) => {
|
|
17873
|
+
const normalized = canonicalScope(scope);
|
|
17874
|
+
if (!normalized)
|
|
17875
|
+
return false;
|
|
17876
|
+
const paths = normalized.paths;
|
|
17877
|
+
return paths.some((item) => item === "." || value === item || value.startsWith(`${item}/`)) && !excluded(value, normalized.exclusions);
|
|
17885
17878
|
};
|
|
17886
|
-
var
|
|
17887
|
-
|
|
17888
|
-
const
|
|
17889
|
-
|
|
17890
|
-
|
|
17891
|
-
|
|
17892
|
-
|
|
17893
|
-
|
|
17894
|
-
return
|
|
17895
|
-
preset,
|
|
17896
|
-
allowed,
|
|
17897
|
-
protected: new Set(merged.protected.map((p) => p.toLowerCase())),
|
|
17898
|
-
integration: wp.integration === "merge" ? "merge" : "pr",
|
|
17899
|
-
defaultTargetBranch: preset === "github-flow" ? "main" : preset === "trunk-based" ? "master" : "develop"
|
|
17900
|
-
};
|
|
17879
|
+
var pathRelevant = (value, scope) => scopeMatches(value, scope);
|
|
17880
|
+
var compareCodeUnits2 = (left, right) => {
|
|
17881
|
+
const length = Math.min(left.length, right.length);
|
|
17882
|
+
for (let index = 0;index < length; index += 1) {
|
|
17883
|
+
const difference = left.charCodeAt(index) - right.charCodeAt(index);
|
|
17884
|
+
if (difference !== 0)
|
|
17885
|
+
return difference;
|
|
17886
|
+
}
|
|
17887
|
+
return left.length - right.length;
|
|
17901
17888
|
};
|
|
17902
|
-
var
|
|
17903
|
-
"
|
|
17904
|
-
|
|
17905
|
-
|
|
17906
|
-
|
|
17907
|
-
|
|
17908
|
-
|
|
17909
|
-
|
|
17910
|
-
|
|
17911
|
-
|
|
17912
|
-
|
|
17913
|
-
|
|
17914
|
-
var readWorkspacesResult = (dir = configDir()) => {
|
|
17915
|
-
const file = path8.join(dir, "workspaces.json");
|
|
17916
|
-
let raw;
|
|
17917
|
-
try {
|
|
17918
|
-
raw = readFileSync5(file, "utf8");
|
|
17919
|
-
} catch {
|
|
17920
|
-
return { status: "missing", path: file, entries: [] };
|
|
17889
|
+
var gitPaths = (root) => {
|
|
17890
|
+
const result = spawnSync2("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
|
|
17891
|
+
cwd: root,
|
|
17892
|
+
encoding: "buffer"
|
|
17893
|
+
});
|
|
17894
|
+
if (result.status !== 0) {
|
|
17895
|
+
const stderr = result.stderr?.toString("utf8") ?? "";
|
|
17896
|
+
return {
|
|
17897
|
+
paths: [],
|
|
17898
|
+
uncertain: !/not a git repository/i.test(stderr),
|
|
17899
|
+
git: /not a git repository/i.test(stderr) ? false : true
|
|
17900
|
+
};
|
|
17921
17901
|
}
|
|
17922
|
-
|
|
17902
|
+
const paths = result.stdout ? result.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path9.sep).join("/")) : [];
|
|
17903
|
+
const stagedDeleted = spawnSync2("git", ["diff", "--cached", "--name-only", "--diff-filter=D", "-z"], { cwd: root, encoding: "buffer" });
|
|
17904
|
+
if (stagedDeleted.status !== 0) {
|
|
17905
|
+
const stderr = stagedDeleted.stderr?.toString("utf8") ?? "";
|
|
17906
|
+
if (!/not a git repository/i.test(stderr))
|
|
17907
|
+
return { paths, uncertain: true, git: true };
|
|
17908
|
+
} else if (stagedDeleted.stdout) {
|
|
17909
|
+
paths.push(...stagedDeleted.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path9.sep).join("/")));
|
|
17910
|
+
}
|
|
17911
|
+
return { paths: [...new Set(paths)], uncertain: false, git: true };
|
|
17912
|
+
};
|
|
17913
|
+
var walk = (root, directory, output) => {
|
|
17914
|
+
let names;
|
|
17923
17915
|
try {
|
|
17924
|
-
|
|
17916
|
+
names = fs7.readdirSync(directory);
|
|
17925
17917
|
} catch {
|
|
17926
|
-
return
|
|
17918
|
+
return true;
|
|
17927
17919
|
}
|
|
17928
|
-
|
|
17929
|
-
|
|
17920
|
+
let uncertain = false;
|
|
17921
|
+
for (const name of names) {
|
|
17922
|
+
if (name === ".git" || name === ".workit")
|
|
17923
|
+
continue;
|
|
17924
|
+
const target = path9.join(directory, name);
|
|
17925
|
+
let stat;
|
|
17926
|
+
try {
|
|
17927
|
+
stat = fs7.lstatSync(target);
|
|
17928
|
+
} catch {
|
|
17929
|
+
uncertain = true;
|
|
17930
|
+
continue;
|
|
17931
|
+
}
|
|
17932
|
+
const item = relative(root, target);
|
|
17933
|
+
output.add(item);
|
|
17934
|
+
if (stat.isDirectory() && !stat.isSymbolicLink())
|
|
17935
|
+
uncertain = walk(root, target, output) || uncertain;
|
|
17930
17936
|
}
|
|
17931
|
-
|
|
17932
|
-
return {
|
|
17933
|
-
status: "valid",
|
|
17934
|
-
path: file,
|
|
17935
|
-
entries: Array.isArray(list) ? list : []
|
|
17936
|
-
};
|
|
17937
|
+
return uncertain;
|
|
17937
17938
|
};
|
|
17938
|
-
var
|
|
17939
|
-
|
|
17940
|
-
|
|
17941
|
-
|
|
17942
|
-
|
|
17943
|
-
|
|
17944
|
-
|
|
17945
|
-
|
|
17946
|
-
|
|
17947
|
-
|
|
17948
|
-
|
|
17949
|
-
|
|
17950
|
-
|
|
17951
|
-
|
|
17952
|
-
if (out.endsWith("/")) {
|
|
17953
|
-
out = out.slice(0, -1);
|
|
17954
|
-
out += "(?:/.*)?";
|
|
17955
|
-
} else {
|
|
17956
|
-
out += ".*";
|
|
17957
|
-
}
|
|
17958
|
-
} else {
|
|
17959
|
-
out += ".*";
|
|
17960
|
-
}
|
|
17961
|
-
i++;
|
|
17962
|
-
}
|
|
17963
|
-
} else {
|
|
17964
|
-
out += "[^/]*";
|
|
17965
|
-
}
|
|
17966
|
-
} else {
|
|
17967
|
-
out += c.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
|
|
17939
|
+
var scopeRoots = (root, scope) => {
|
|
17940
|
+
const roots = [];
|
|
17941
|
+
for (const item of scope.paths.length ? scope.paths : ["."]) {
|
|
17942
|
+
const target = path9.resolve(root, item);
|
|
17943
|
+
if (!inside(root, target))
|
|
17944
|
+
return failure("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` }] });
|
|
17945
|
+
let ancestor = target;
|
|
17946
|
+
while (!fs7.existsSync(ancestor) && ancestor !== root)
|
|
17947
|
+
ancestor = path9.dirname(ancestor);
|
|
17948
|
+
try {
|
|
17949
|
+
if (!inside(root, fs7.realpathSync(ancestor)))
|
|
17950
|
+
return failure("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` }] });
|
|
17951
|
+
} catch {
|
|
17952
|
+
return failure("invalid_input", `candidate scope cannot be inspected: ${item}`);
|
|
17968
17953
|
}
|
|
17954
|
+
roots.push(target);
|
|
17969
17955
|
}
|
|
17970
|
-
return
|
|
17956
|
+
return success(null, null, roots);
|
|
17971
17957
|
};
|
|
17972
|
-
|
|
17973
|
-
|
|
17974
|
-
const real = realpathSync4.native ?? realpathSync4;
|
|
17958
|
+
function captureCandidate(root, scope, environment = []) {
|
|
17959
|
+
let checkout;
|
|
17975
17960
|
try {
|
|
17976
|
-
|
|
17961
|
+
checkout = fs7.realpathSync(root);
|
|
17977
17962
|
} catch {
|
|
17978
|
-
return
|
|
17963
|
+
return failure("invalid_input", "candidate root does not exist");
|
|
17979
17964
|
}
|
|
17980
|
-
|
|
17981
|
-
|
|
17982
|
-
|
|
17983
|
-
const
|
|
17984
|
-
|
|
17985
|
-
|
|
17986
|
-
|
|
17987
|
-
const
|
|
17988
|
-
|
|
17989
|
-
|
|
17990
|
-
|
|
17991
|
-
|
|
17992
|
-
for (const entry of loadWorkspacesFrom(dir)) {
|
|
17993
|
-
if (!entry || typeof entry !== "object")
|
|
17965
|
+
const normalizedScope = canonicalScope(scope);
|
|
17966
|
+
if (!normalizedScope)
|
|
17967
|
+
return failure("invalid_input", "candidate scope is invalid: paths must be checkout-relative — to work in another repository, keep this task here and coordinate a separate linked task in that checkout", { fields: [{ path: "scope.paths", reason: "must be checkout-relative" }] });
|
|
17968
|
+
const roots = scopeRoots(checkout, normalizedScope);
|
|
17969
|
+
if (!roots.ok)
|
|
17970
|
+
return roots;
|
|
17971
|
+
const git = gitPaths(checkout);
|
|
17972
|
+
const names = new Set;
|
|
17973
|
+
let uncertain = git.uncertain;
|
|
17974
|
+
for (const target of roots.data) {
|
|
17975
|
+
const item = relative(checkout, target);
|
|
17976
|
+
if (item === ".git" || item === ".workit")
|
|
17994
17977
|
continue;
|
|
17995
|
-
|
|
17996
|
-
if (
|
|
17978
|
+
names.add(item);
|
|
17979
|
+
if (!fs7.existsSync(target))
|
|
17997
17980
|
continue;
|
|
17998
|
-
|
|
17999
|
-
|
|
18000
|
-
|
|
18001
|
-
|
|
18002
|
-
|
|
18003
|
-
|
|
18004
|
-
|
|
17981
|
+
let stat;
|
|
17982
|
+
try {
|
|
17983
|
+
stat = fs7.lstatSync(target);
|
|
17984
|
+
} catch {
|
|
17985
|
+
uncertain = true;
|
|
17986
|
+
continue;
|
|
17987
|
+
}
|
|
17988
|
+
if (!git.git && stat.isDirectory() && !stat.isSymbolicLink())
|
|
17989
|
+
uncertain = walk(checkout, target, names) || uncertain;
|
|
17990
|
+
}
|
|
17991
|
+
for (const item of git.paths)
|
|
17992
|
+
if (scopeMatches(item, normalizedScope))
|
|
17993
|
+
names.add(item);
|
|
17994
|
+
const files = [];
|
|
17995
|
+
let completeness = uncertain ? "uncertain" : "known";
|
|
17996
|
+
for (const item of [...names].sort()) {
|
|
17997
|
+
if (!scopeMatches(item, normalizedScope))
|
|
17998
|
+
continue;
|
|
17999
|
+
const target = path9.join(checkout, item);
|
|
18000
|
+
try {
|
|
18001
|
+
const stat = fs7.lstatSync(target);
|
|
18002
|
+
if (stat.isSymbolicLink()) {
|
|
18003
|
+
const link = fs7.readlinkSync(target);
|
|
18004
|
+
files.push({ path: item, kind: "symlink", digest: digestBytes3(link), executable: null });
|
|
18005
|
+
} else if (stat.isFile()) {
|
|
18006
|
+
files.push({
|
|
18007
|
+
path: item,
|
|
18008
|
+
kind: "file",
|
|
18009
|
+
digest: digestBytes3(fs7.readFileSync(target)),
|
|
18010
|
+
executable: (stat.mode & 73) !== 0
|
|
18011
|
+
});
|
|
18012
|
+
} else if (!stat.isDirectory()) {
|
|
18013
|
+
completeness = "uncertain";
|
|
18014
|
+
} else if (git.git && git.paths.includes(item)) {
|
|
18015
|
+
completeness = "uncertain";
|
|
18016
|
+
}
|
|
18017
|
+
} catch (error) {
|
|
18018
|
+
if (error?.code === "ENOENT") {
|
|
18019
|
+
files.push({ path: item, kind: "absent", digest: null, executable: null });
|
|
18020
|
+
} else
|
|
18021
|
+
completeness = "uncertain";
|
|
18005
18022
|
}
|
|
18006
18023
|
}
|
|
18007
|
-
|
|
18024
|
+
const supplied = Array.isArray(environment) ? environment.map((value) => typeof value === "string" ? { name: value, value: process.env[value] ?? null } : { name: value.name, value: value.value ?? null }) : Object.keys(environment).map((name) => ({
|
|
18025
|
+
name,
|
|
18026
|
+
value: environment[name] ?? null
|
|
18027
|
+
}));
|
|
18028
|
+
const namesByEnvironment = supplied.map(({ name }) => name);
|
|
18029
|
+
if (new Set(namesByEnvironment).size !== namesByEnvironment.length || namesByEnvironment.some((name) => typeof name !== "string" || !name))
|
|
18030
|
+
return failure("invalid_input", "candidate environment names must be unique and non-empty");
|
|
18031
|
+
const values = supplied.sort((left, right) => compareCodeUnits2(left.name, right.name)).map(({ name, value }) => ({ name, value, refs: [] }));
|
|
18032
|
+
if (values.some(({ value }) => value === null))
|
|
18033
|
+
completeness = "uncertain";
|
|
18034
|
+
const headResult = spawnSync2("git", ["rev-parse", "HEAD"], { cwd: checkout, encoding: "utf8" });
|
|
18035
|
+
if (headResult.status !== 0 && !/not a git repository/i.test(headResult.stderr ?? ""))
|
|
18036
|
+
completeness = "uncertain";
|
|
18037
|
+
const head = headResult.status === 0 ? headResult.stdout.trim() : null;
|
|
18038
|
+
const partial = {
|
|
18039
|
+
id: "0".repeat(64),
|
|
18040
|
+
scope: normalizedScope,
|
|
18041
|
+
completeness,
|
|
18042
|
+
files,
|
|
18043
|
+
environment: values,
|
|
18044
|
+
head
|
|
18045
|
+
};
|
|
18046
|
+
const candidate = { ...partial, id: candidateDigest(partial) };
|
|
18047
|
+
const parsed = candidateSchema.safeParse(candidate);
|
|
18048
|
+
return parsed.success ? success(null, null, parsed.data) : failure("invalid_input", "captured candidate is invalid");
|
|
18049
|
+
}
|
|
18050
|
+
var fileMap = (candidate) => new Map(candidate.files.map((file) => [file.path, file]));
|
|
18051
|
+
var changedPaths = (before, after) => {
|
|
18052
|
+
const paths = new Set([...before.files, ...after.files].map((file) => file.path));
|
|
18053
|
+
const left = fileMap(before);
|
|
18054
|
+
const right = fileMap(after);
|
|
18055
|
+
const changed = [...paths].filter((item) => JSON.stringify(left.get(item) ?? null) !== JSON.stringify(right.get(item) ?? null));
|
|
18056
|
+
if (before.head !== after.head)
|
|
18057
|
+
changed.push(".");
|
|
18058
|
+
if (JSON.stringify(before.environment) !== JSON.stringify(after.environment))
|
|
18059
|
+
changed.push(".");
|
|
18060
|
+
return changed;
|
|
18008
18061
|
};
|
|
18009
|
-
var
|
|
18010
|
-
|
|
18011
|
-
// packages/workit-core/src/core/vcs-config.ts
|
|
18012
|
-
import fs7 from "node:fs";
|
|
18013
|
-
import path9 from "node:path";
|
|
18014
|
-
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
18015
|
-
var TOKEN_PLACEHOLDER = "YOUR_TOKEN_HERE";
|
|
18016
|
-
var vcsConfigPath = () => process.env.WORKFLOW_VCS_CONFIG ?? path9.join(configDir(), "vcs.json");
|
|
18017
|
-
var vcsCwd = (cwd) => cwd ?? process.env.WORKFLOW_WORKSPACE_ROOT ?? process.cwd();
|
|
18018
|
-
var remoteProvider = (cwd) => {
|
|
18019
|
-
const r = spawnSync2("git", ["remote", "get-url", "origin"], { cwd, encoding: "utf8" });
|
|
18020
|
-
if (r.status !== 0)
|
|
18062
|
+
var sessionFromRef = (value) => {
|
|
18063
|
+
if (typeof value !== "object" || value === null || value.kind !== "host")
|
|
18021
18064
|
return null;
|
|
18022
|
-
|
|
18023
|
-
if (/github\.com[:/]/.test(url))
|
|
18024
|
-
return "github";
|
|
18025
|
-
if (/gitlab\.com[:/]/.test(url))
|
|
18026
|
-
return "gitlab";
|
|
18027
|
-
return null;
|
|
18065
|
+
return typeof value.host === "string" && typeof value.handle === "string" ? { host: value.host, handle: value.handle } : null;
|
|
18028
18066
|
};
|
|
18029
|
-
var
|
|
18030
|
-
const
|
|
18031
|
-
|
|
18032
|
-
try {
|
|
18033
|
-
raw = fs7.readFileSync(file, "utf8");
|
|
18034
|
-
} catch {
|
|
18035
|
-
return { status: "missing", path: file, config: {} };
|
|
18036
|
-
}
|
|
18037
|
-
let parsed;
|
|
18038
|
-
try {
|
|
18039
|
-
parsed = JSON.parse(raw);
|
|
18040
|
-
} catch {
|
|
18041
|
-
return { status: "malformed", path: file, config: {}, error: `${file} is not valid JSON` };
|
|
18042
|
-
}
|
|
18043
|
-
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
18044
|
-
return { status: "valid", path: file, config: parsed };
|
|
18045
|
-
}
|
|
18046
|
-
return { status: "malformed", path: file, config: {}, error: `${file} is not a JSON object` };
|
|
18067
|
+
var sameSession = (left, right) => {
|
|
18068
|
+
const other = sessionFromRef(right);
|
|
18069
|
+
return left !== null && other !== null && left.host === other.host && left.handle === other.handle;
|
|
18047
18070
|
};
|
|
18048
|
-
|
|
18049
|
-
|
|
18050
|
-
const
|
|
18051
|
-
|
|
18052
|
-
|
|
18053
|
-
|
|
18054
|
-
|
|
18055
|
-
|
|
18056
|
-
|
|
18057
|
-
|
|
18058
|
-
|
|
18059
|
-
|
|
18060
|
-
|
|
18061
|
-
|
|
18062
|
-
|
|
18063
|
-
|
|
18064
|
-
|
|
18065
|
-
|
|
18066
|
-
|
|
18067
|
-
|
|
18068
|
-
|
|
18069
|
-
|
|
18070
|
-
|
|
18071
|
-
|
|
18072
|
-
|
|
18071
|
+
var evidenceScopes = (task, ids) => (task.policy?.requirements ?? []).filter((requirement) => ids.includes(requirement.id)).map((requirement) => requirement.scope);
|
|
18072
|
+
function evaluateEvidence(task, candidate) {
|
|
18073
|
+
const current = candidate ?? task.candidates.at(-1) ?? null;
|
|
18074
|
+
return task.evidence.map((entry) => {
|
|
18075
|
+
const evidence = entry.data;
|
|
18076
|
+
if (evidence.result === "failed")
|
|
18077
|
+
return {
|
|
18078
|
+
evidenceId: entry.id,
|
|
18079
|
+
status: "failed",
|
|
18080
|
+
reason: "failed evidence remains historical"
|
|
18081
|
+
};
|
|
18082
|
+
if (evidence.kind === "check" || evidence.kind === "review") {
|
|
18083
|
+
if (!evidence.beforeCandidateId || !evidence.candidateId)
|
|
18084
|
+
return {
|
|
18085
|
+
evidenceId: entry.id,
|
|
18086
|
+
status: "stale",
|
|
18087
|
+
reason: "check or review evidence lacks start and end candidates"
|
|
18088
|
+
};
|
|
18089
|
+
if (evidence.beforeCandidateId !== evidence.candidateId)
|
|
18090
|
+
return {
|
|
18091
|
+
evidenceId: entry.id,
|
|
18092
|
+
status: "stale",
|
|
18093
|
+
reason: "candidate changed during the check or review"
|
|
18094
|
+
};
|
|
18095
|
+
}
|
|
18096
|
+
if (!evidence.candidateId || !current)
|
|
18097
|
+
return {
|
|
18098
|
+
evidenceId: entry.id,
|
|
18099
|
+
status: evidence.result,
|
|
18100
|
+
reason: "evidence has no candidate binding"
|
|
18101
|
+
};
|
|
18102
|
+
const observed = task.candidates.find((item) => item.id === evidence.candidateId) ?? (current.id === evidence.candidateId ? current : undefined);
|
|
18103
|
+
if (!observed || observed.completeness === "uncertain" || current.completeness === "uncertain")
|
|
18104
|
+
return { evidenceId: entry.id, status: "stale", reason: "candidate is missing or uncertain" };
|
|
18105
|
+
if (observed.id === current.id)
|
|
18106
|
+
return { evidenceId: entry.id, status: evidence.result, reason: "candidate is unchanged" };
|
|
18107
|
+
const scopes = evidenceScopes(task, evidence.requirementIds);
|
|
18108
|
+
const changed = changedPaths(observed, current);
|
|
18109
|
+
const environmentChanged = JSON.stringify(observed.environment) !== JSON.stringify(current.environment);
|
|
18110
|
+
if (!scopes.length || environmentChanged || changed.some((item) => scopes.some((scope) => pathRelevant(item, scope))))
|
|
18111
|
+
return { evidenceId: entry.id, status: "stale", reason: "relevant candidate state changed" };
|
|
18073
18112
|
return {
|
|
18074
|
-
|
|
18075
|
-
|
|
18076
|
-
|
|
18077
|
-
defaultTargetBranch: defaultTarget,
|
|
18078
|
-
link_issues: linkIssues,
|
|
18079
|
-
youtrack_base_url: youtrackBaseUrl,
|
|
18080
|
-
issues_provider: issuesProvider,
|
|
18081
|
-
link_on_pr: linkOnPr
|
|
18113
|
+
evidenceId: entry.id,
|
|
18114
|
+
status: evidence.result,
|
|
18115
|
+
reason: "candidate changed outside evidence scope"
|
|
18082
18116
|
};
|
|
18083
|
-
}
|
|
18084
|
-
|
|
18085
|
-
|
|
18086
|
-
|
|
18087
|
-
|
|
18088
|
-
|
|
18089
|
-
|
|
18090
|
-
|
|
18117
|
+
});
|
|
18118
|
+
}
|
|
18119
|
+
var scopeCovers2 = scopeCovers;
|
|
18120
|
+
var checkPin = (assignment, beforeCandidateId, candidateId) => {
|
|
18121
|
+
const pin = assignment.candidateId ?? null;
|
|
18122
|
+
if (pin === null)
|
|
18123
|
+
return true;
|
|
18124
|
+
return (beforeCandidateId ?? null) === pin || (candidateId ?? null) === pin;
|
|
18125
|
+
};
|
|
18126
|
+
var resolveBinding = (evidence, pin, currentCandidateId) => {
|
|
18127
|
+
const bound = evidence.kind === "check" || evidence.kind === "review";
|
|
18128
|
+
const fallback = bound ? pin ?? currentCandidateId : null;
|
|
18129
|
+
return {
|
|
18130
|
+
beforeCandidateId: evidence.beforeCandidateId ?? fallback,
|
|
18131
|
+
candidateId: evidence.candidateId ?? fallback
|
|
18132
|
+
};
|
|
18133
|
+
};
|
|
18134
|
+
var findingVerificationPasses = (findingCandidateId, evidence) => evidence.status === "passed" && (evidence.kind === "check" || evidence.kind === "review") && ((findingCandidateId ?? null) === null || evidence.candidateId === findingCandidateId);
|
|
18135
|
+
var applicableDecision2 = (task, workspace, requirement, checkoutRoot) => task.decisions.filter((entry) => entry.provenance.kind !== "imported").map((entry) => entry.data).filter((decision) => decision.purpose === "limitation" && decision.response === "approved" && decision.revoked === null && decision.binding.taskId === task.id && decision.binding.workspaceId === workspace.id && decision.requirementIds.includes(requirement.id) && decision.binding.scope && scopeCovers2(decision.binding.scope, requirement.scope) && decision.digest === decisionDigest(decision) && (checkoutRoot ? verifyDecisionContentAtRoot(checkoutRoot, decision.binding).ok : decision.binding.contentRefs.every((reference) => reference.kind !== "file")));
|
|
18136
|
+
var applicableRequirementDecision = (task, workspace, requirement, checkoutRoot) => task.decisions.filter(({ data, provenance }) => provenance.kind !== "imported" && data.purpose !== "limitation" && (data.response === "approved" || data.response === "stated") && data.revoked === null && data.binding.taskId === task.id && data.binding.workspaceId === workspace.id && data.requirementIds.includes(requirement.id) && scopeCovers2(data.binding.scope, requirement.scope) && data.digest === decisionDigest(data) && (checkoutRoot ? verifyDecisionContentAtRoot(checkoutRoot, data.binding).ok : data.binding.contentRefs.every((reference) => reference.kind !== "file"))).map(({ id }) => ({ id }));
|
|
18137
|
+
var evidenceMatchesRequirement = (kind, evidenceKind, dimension, ruleId) => {
|
|
18138
|
+
if (kind !== "passed")
|
|
18139
|
+
return false;
|
|
18140
|
+
if (dimension === "testing" || dimension === "verification")
|
|
18141
|
+
return evidenceKind === "check" || ruleId === "pre-pr-cleanup" && evidenceKind === "artifact";
|
|
18142
|
+
if (dimension === "review")
|
|
18143
|
+
return evidenceKind === "review";
|
|
18144
|
+
if (dimension === "investigation" || dimension === "challenge")
|
|
18145
|
+
return evidenceKind === "investigation";
|
|
18146
|
+
if (dimension === "artifacts" || dimension === "continuity")
|
|
18147
|
+
return evidenceKind === "artifact";
|
|
18148
|
+
return false;
|
|
18149
|
+
};
|
|
18150
|
+
var expectedKinds = (dimension, ruleId) => {
|
|
18151
|
+
if (dimension === "testing")
|
|
18152
|
+
return "check";
|
|
18153
|
+
if (dimension === "verification")
|
|
18154
|
+
return ruleId === "pre-pr-cleanup" ? "check or artifact" : "check";
|
|
18155
|
+
if (dimension === "review")
|
|
18156
|
+
return "review";
|
|
18157
|
+
if (dimension === "investigation" || dimension === "challenge")
|
|
18158
|
+
return "investigation";
|
|
18159
|
+
if (dimension === "artifacts" || dimension === "continuity")
|
|
18160
|
+
return "artifact";
|
|
18161
|
+
if (dimension === "decisions")
|
|
18162
|
+
return "decision";
|
|
18163
|
+
return "worker report";
|
|
18164
|
+
};
|
|
18165
|
+
function evaluateRequirements(task, workspace, capabilities, candidate, checkoutRoot, _caller) {
|
|
18166
|
+
const evidence = evaluateEvidence(task, candidate);
|
|
18167
|
+
if (task.policy && task.policy.policyVersion !== POLICY_VERSION)
|
|
18168
|
+
return task.policy.requirements.map((requirement) => ({
|
|
18169
|
+
requirementId: requirement.id,
|
|
18170
|
+
status: "unsatisfied",
|
|
18171
|
+
evidenceIds: [],
|
|
18172
|
+
decisionIds: [],
|
|
18173
|
+
reason: "stored policy version is unsupported; reassessment is required"
|
|
18174
|
+
}));
|
|
18175
|
+
return (task.policy?.requirements ?? []).map((requirement) => {
|
|
18176
|
+
const related = task.evidence.map((entry, index) => ({ entry, evaluation: evidence[index] })).filter(({ entry }) => entry.data.requirementIds.includes(requirement.id));
|
|
18177
|
+
const passed = related.filter(({ entry, evaluation }) => {
|
|
18178
|
+
if (!evidenceMatchesRequirement(evaluation.status, entry.data.kind, requirement.dimension, requirement.ruleId))
|
|
18179
|
+
return false;
|
|
18180
|
+
if ((entry.data.kind === "check" || entry.data.kind === "review") && (!entry.data.beforeCandidateId || !entry.data.candidateId || entry.data.beforeCandidateId !== entry.data.candidateId))
|
|
18181
|
+
return false;
|
|
18182
|
+
if (requirement.dimension !== "review")
|
|
18183
|
+
return true;
|
|
18184
|
+
const reviewSession = sessionFromRef(entry.data.reviewContext);
|
|
18185
|
+
const sameImplementation = sameSession(reviewSession, task.intent.provenance.session);
|
|
18186
|
+
const sameEvidenceSession = related.some(({ entry: other }) => other.id !== entry.id && other.data.kind === "review" && other.data.candidateId !== null && other.data.candidateId === entry.data.candidateId && sameSession(reviewSession, other.provenance.session));
|
|
18187
|
+
return entry.data.kind === "review" && reviewSession !== null && sameSession(reviewSession, entry.provenance.session) && (requirement.ruleId === "self-review" || !sameImplementation && !sameEvidenceSession);
|
|
18188
|
+
});
|
|
18189
|
+
if (passed.length) {
|
|
18190
|
+
const currentId = candidate?.id ?? null;
|
|
18191
|
+
const withRed = requirement.dimension === "testing" ? passed.filter(({ entry }) => related.some((other) => other.entry.data.kind === "check" && other.evaluation.status === "failed" && other.entry.data.requirementIds.includes(requirement.id) && other.entry.recordedAt <= entry.recordedAt) || related.some((baseline) => baseline.entry.id !== entry.id && baseline.entry.data.kind === "check" && baseline.entry.data.result === "passed" && baseline.entry.data.candidateId !== null && currentId !== null && baseline.entry.data.candidateId !== currentId)) : passed;
|
|
18192
|
+
if (withRed.length)
|
|
18193
|
+
return {
|
|
18194
|
+
requirementId: requirement.id,
|
|
18195
|
+
status: "satisfied",
|
|
18196
|
+
evidenceIds: withRed.map(({ entry }) => entry.id),
|
|
18197
|
+
decisionIds: [],
|
|
18198
|
+
reason: "fresh applicable evidence passed"
|
|
18199
|
+
};
|
|
18200
|
+
if (requirement.dimension === "testing")
|
|
18201
|
+
return {
|
|
18202
|
+
requirementId: requirement.id,
|
|
18203
|
+
status: "unsatisfied",
|
|
18204
|
+
evidenceIds: [],
|
|
18205
|
+
decisionIds: [],
|
|
18206
|
+
reason: "testing requirement has GREEN evidence but no preceding RED"
|
|
18207
|
+
};
|
|
18208
|
+
}
|
|
18209
|
+
if (requirement.dimension === "delegation") {
|
|
18210
|
+
const dot = (scope) => scope.paths.length > 0 ? scope : { ...scope, paths: ["."] };
|
|
18211
|
+
const reconciled = task.workers.some((worker) => worker.data.report?.outcome === "completed" && (worker.data.assignment.requirementIds.includes(requirement.id) || scopeCovers2(dot(requirement.scope), dot(worker.data.assignment.scope))));
|
|
18212
|
+
if (reconciled)
|
|
18213
|
+
return {
|
|
18214
|
+
requirementId: requirement.id,
|
|
18215
|
+
status: "satisfied",
|
|
18216
|
+
evidenceIds: [],
|
|
18217
|
+
decisionIds: [],
|
|
18218
|
+
reason: "bounded helper completed and its result is recorded"
|
|
18219
|
+
};
|
|
18220
|
+
}
|
|
18221
|
+
const decisions = requirement.dimension === "decisions" ? applicableRequirementDecision(task, workspace, requirement, checkoutRoot) : [];
|
|
18222
|
+
if (decisions.length)
|
|
18223
|
+
return {
|
|
18224
|
+
requirementId: requirement.id,
|
|
18225
|
+
status: "satisfied",
|
|
18226
|
+
evidenceIds: [],
|
|
18227
|
+
decisionIds: decisions.map((decision) => decision.id),
|
|
18228
|
+
reason: "an applicable approved or stated decision satisfies the requirement"
|
|
18229
|
+
};
|
|
18230
|
+
const limitations = requirement.acceptanceAllowed ? applicableDecision2(task, workspace, requirement, checkoutRoot) : [];
|
|
18231
|
+
if (limitations.length)
|
|
18232
|
+
return {
|
|
18233
|
+
requirementId: requirement.id,
|
|
18234
|
+
status: "accepted_limitation",
|
|
18235
|
+
evidenceIds: [],
|
|
18236
|
+
decisionIds: limitations.map((decision) => task.decisions.find((entry) => entry.data === decision).id),
|
|
18237
|
+
reason: "an applicable approved limitation permits the missing evidence"
|
|
18238
|
+
};
|
|
18239
|
+
const unavailable = capabilities.some((capability) => capability.assurance === "unavailable" && (capability.name === requirement.dimension || capability.surface === requirement.dimension));
|
|
18240
|
+
if (!unavailable && related.length > 0 && !passed.length)
|
|
18241
|
+
return {
|
|
18242
|
+
requirementId: requirement.id,
|
|
18243
|
+
status: "unsatisfied",
|
|
18244
|
+
evidenceIds: related.map(({ entry }) => entry.id),
|
|
18245
|
+
decisionIds: [],
|
|
18246
|
+
reason: `no passing evidence (needs kind:${expectedKinds(requirement.dimension, requirement.ruleId)})`
|
|
18247
|
+
};
|
|
18091
18248
|
return {
|
|
18092
|
-
|
|
18093
|
-
|
|
18094
|
-
|
|
18095
|
-
|
|
18096
|
-
|
|
18097
|
-
const prov = cfg[provider] ?? {};
|
|
18098
|
-
const wsTokenFile = typeof wsVcs.tokenFile === "string" && wsVcs.tokenFile.trim() !== "" ? wsVcs.tokenFile : null;
|
|
18099
|
-
const tokenFile = String(wsTokenFile ?? prov.tokenFile ?? path9.join(configDir(), `${provider}.token`));
|
|
18100
|
-
const tokenPath = path9.resolve(tokenFile);
|
|
18101
|
-
let tokenOk = false;
|
|
18102
|
-
if (fs7.existsSync(tokenPath)) {
|
|
18103
|
-
const token = fs7.readFileSync(tokenPath, "utf8").trim();
|
|
18104
|
-
const placeholder = !token || token === TOKEN_PLACEHOLDER || token.startsWith(TOKEN_PLACEHOLDER);
|
|
18105
|
-
tokenOk = !placeholder;
|
|
18106
|
-
}
|
|
18107
|
-
const out = {
|
|
18108
|
-
ok: true,
|
|
18109
|
-
configPath: path9.resolve(cfgPath),
|
|
18110
|
-
provider,
|
|
18111
|
-
defaultTargetBranch: defaultTarget,
|
|
18112
|
-
pr: cfg.pr ?? {},
|
|
18113
|
-
tokenPath,
|
|
18114
|
-
tokenPresent: fs7.existsSync(tokenPath),
|
|
18115
|
-
tokenReady: tokenOk,
|
|
18116
|
-
workspace_name: ws?.name ?? null,
|
|
18117
|
-
link_issues: linkIssues,
|
|
18118
|
-
youtrack_base_url: youtrackBaseUrl,
|
|
18119
|
-
issues_provider: issuesProvider,
|
|
18120
|
-
link_on_pr: linkOnPr
|
|
18121
|
-
};
|
|
18122
|
-
if (provider === "gitlab") {
|
|
18123
|
-
out.gitlab = {
|
|
18124
|
-
host: prov.host ?? "gitlab.com",
|
|
18125
|
-
apiUrl: prov.apiUrl ?? "https://gitlab.com/api/v4"
|
|
18249
|
+
requirementId: requirement.id,
|
|
18250
|
+
status: unavailable ? "unavailable" : "unsatisfied",
|
|
18251
|
+
evidenceIds: related.map(({ entry }) => entry.id),
|
|
18252
|
+
decisionIds: [],
|
|
18253
|
+
reason: unavailable ? "required capability is unavailable" : "no fresh passing evidence"
|
|
18126
18254
|
};
|
|
18127
|
-
} else if (provider === "github") {
|
|
18128
|
-
out.github = { host: prov.host ?? "github.com" };
|
|
18129
|
-
}
|
|
18130
|
-
if (mode === "summary")
|
|
18131
|
-
delete out.tokenPath;
|
|
18132
|
-
return out;
|
|
18133
|
-
}
|
|
18134
|
-
function mergedPrStyle(limit = 6, cwd) {
|
|
18135
|
-
const cfg = vcsConfig("load", cwd);
|
|
18136
|
-
if (!cfg.ok || !cfg.tokenReady)
|
|
18137
|
-
return { ok: false, error: "vcs not configured" };
|
|
18138
|
-
const provider = cfg.provider;
|
|
18139
|
-
const token = fs7.readFileSync(cfg.tokenPath, "utf8").trim();
|
|
18140
|
-
const examples = [];
|
|
18141
|
-
const descInfo = (desc, caseInsensitiveNotes = false) => ({
|
|
18142
|
-
hasNotesSection: caseInsensitiveNotes ? /##\s*notes/i.test(desc) : /##\s*Notes/.test(desc),
|
|
18143
|
-
sections: desc.split(`
|
|
18144
|
-
`).filter((l) => l.startsWith("## ")).map((l) => l.trim()),
|
|
18145
|
-
descriptionPreview: desc.slice(0, 600)
|
|
18146
18255
|
});
|
|
18147
|
-
|
|
18148
|
-
|
|
18149
|
-
|
|
18150
|
-
|
|
18151
|
-
|
|
18152
|
-
|
|
18153
|
-
|
|
18154
|
-
|
|
18155
|
-
|
|
18156
|
-
|
|
18157
|
-
|
|
18158
|
-
|
|
18159
|
-
|
|
18160
|
-
|
|
18161
|
-
|
|
18162
|
-
|
|
18163
|
-
|
|
18164
|
-
|
|
18165
|
-
|
|
18166
|
-
|
|
18167
|
-
|
|
18168
|
-
|
|
18169
|
-
|
|
18170
|
-
|
|
18171
|
-
|
|
18172
|
-
|
|
18173
|
-
}
|
|
18174
|
-
|
|
18175
|
-
|
|
18176
|
-
|
|
18177
|
-
|
|
18178
|
-
|
|
18179
|
-
|
|
18180
|
-
|
|
18181
|
-
|
|
18256
|
+
}
|
|
18257
|
+
function evaluateClosure(requestedOutcome, view) {
|
|
18258
|
+
if (requestedOutcome !== "stopped" && !view.task.policy)
|
|
18259
|
+
return failure("requirements_unsatisfied", "task has not been assessed");
|
|
18260
|
+
if (view.task.policy && view.task.policy.policyVersion !== POLICY_VERSION)
|
|
18261
|
+
return failure("requirements_unsatisfied", "stored policy version is unsupported; reassessment is required");
|
|
18262
|
+
const evaluations = view.requirements;
|
|
18263
|
+
const openFindings = view.task.findings.filter((entry) => entry.data.disposition === "open");
|
|
18264
|
+
if (requestedOutcome !== "stopped" && openFindings.length)
|
|
18265
|
+
return failure("requirements_unsatisfied", "open findings must be resolved before closure");
|
|
18266
|
+
if (requestedOutcome !== "stopped") {
|
|
18267
|
+
const evidenceById = new Map(view.evidence.map((entry) => [entry.evidenceId, entry.status]));
|
|
18268
|
+
for (const entry of view.task.findings) {
|
|
18269
|
+
if (entry.data.disposition === "fixed") {
|
|
18270
|
+
const verified = entry.data.resolution?.evidenceIds.some((id) => {
|
|
18271
|
+
const evidence = view.task.evidence.find((item) => item.id === id);
|
|
18272
|
+
if (!evidence)
|
|
18273
|
+
return false;
|
|
18274
|
+
return findingVerificationPasses(entry.data.candidateId, {
|
|
18275
|
+
kind: evidence.data.kind,
|
|
18276
|
+
candidateId: evidence.data.candidateId,
|
|
18277
|
+
status: evidenceById.get(id) ?? "missing"
|
|
18278
|
+
});
|
|
18279
|
+
});
|
|
18280
|
+
if (!verified)
|
|
18281
|
+
return failure("requirements_unsatisfied", "fixed findings require current verification");
|
|
18282
|
+
}
|
|
18283
|
+
if (entry.data.disposition === "deferred") {
|
|
18284
|
+
const valid = entry.data.resolution?.decisionIds.some((id) => {
|
|
18285
|
+
const decisionEntry = view.task.decisions.find((item) => item.id === id);
|
|
18286
|
+
const decision = decisionEntry?.data;
|
|
18287
|
+
return Boolean(decision && decisionEntry?.provenance.kind !== "imported" && decision.purpose === "limitation" && decision.response === "approved" && decision.revoked === null && decision.digest === decisionDigest(decision) && verifyDecisionContentAtRoot(view.workspace.root, decision.binding).ok && decision.binding.taskId === view.task.id && decision.binding.workspaceId === view.workspace.id && scopeCovers2(decision.binding.scope, entry.data.scope) && decision.requirementIds.some((requirementId) => view.task.policy?.requirements.some((requirement) => requirement.id === requirementId && requirement.acceptanceAllowed)));
|
|
18288
|
+
});
|
|
18289
|
+
if (!valid)
|
|
18290
|
+
return failure("requirements_unsatisfied", "deferred findings require an approved limitation decision that references an acceptanceAllowed requirement and covers the finding scope");
|
|
18291
|
+
}
|
|
18182
18292
|
}
|
|
18183
18293
|
}
|
|
18184
|
-
|
|
18185
|
-
|
|
18186
|
-
|
|
18187
|
-
|
|
18188
|
-
|
|
18189
|
-
|
|
18190
|
-
|
|
18191
|
-
|
|
18192
|
-
|
|
18193
|
-
|
|
18194
|
-
|
|
18294
|
+
const blocking = evaluations.filter((item) => {
|
|
18295
|
+
if (item.status !== "unsatisfied" && item.status !== "unavailable")
|
|
18296
|
+
return false;
|
|
18297
|
+
const requirement = view.task.policy?.requirements.find((candidate) => candidate.id === item.requirementId);
|
|
18298
|
+
return !requirement || requirement.before === "close";
|
|
18299
|
+
});
|
|
18300
|
+
if (requestedOutcome !== "stopped" && blocking.length)
|
|
18301
|
+
return failure("requirements_unsatisfied", "applicable requirements are unsatisfied", {
|
|
18302
|
+
requirementIds: blocking.map((item) => item.requirementId),
|
|
18303
|
+
requirements: blocking.map((item) => {
|
|
18304
|
+
const requirement = view.task.policy?.requirements.find((candidate) => candidate.id === item.requirementId);
|
|
18305
|
+
return {
|
|
18306
|
+
requirementId: item.requirementId,
|
|
18307
|
+
ruleId: requirement?.ruleId ?? "unknown",
|
|
18308
|
+
reason: item.reason,
|
|
18309
|
+
satisfaction: requirement?.satisfaction ?? "",
|
|
18310
|
+
dependentAction: requirement?.dependentAction ?? null
|
|
18311
|
+
};
|
|
18312
|
+
})
|
|
18313
|
+
});
|
|
18314
|
+
const accepted = evaluations.filter((item) => item.status === "accepted_limitation");
|
|
18315
|
+
if (requestedOutcome === "verified" && accepted.length)
|
|
18316
|
+
return failure("requirements_unsatisfied", "accepted limitations cannot be reported as verified", {
|
|
18317
|
+
requirementIds: accepted.map((item) => item.requirementId)
|
|
18318
|
+
});
|
|
18319
|
+
const evidenceIds = [
|
|
18320
|
+
...new Set([
|
|
18321
|
+
...evaluations.flatMap((item) => item.evidenceIds),
|
|
18322
|
+
...view.task.findings.flatMap((item) => item.data.resolution?.evidenceIds ?? [])
|
|
18323
|
+
])
|
|
18324
|
+
];
|
|
18325
|
+
const decisionIds = [
|
|
18326
|
+
...new Set([
|
|
18327
|
+
...evaluations.flatMap((item) => item.decisionIds),
|
|
18328
|
+
...view.task.findings.flatMap((item) => item.data.resolution?.decisionIds ?? [])
|
|
18329
|
+
])
|
|
18330
|
+
];
|
|
18331
|
+
return success(null, null, {
|
|
18332
|
+
outcome: requestedOutcome === "stopped" ? "stopped" : accepted.length ? "accepted_limitations" : "verified",
|
|
18333
|
+
evidenceIds,
|
|
18334
|
+
decisionIds,
|
|
18335
|
+
requirementIds: evaluations.map((item) => item.requirementId)
|
|
18336
|
+
});
|
|
18195
18337
|
}
|
|
18196
18338
|
|
|
18197
|
-
// packages/workit-core/src/core/
|
|
18198
|
-
|
|
18339
|
+
// packages/workit-core/src/core/task-context.ts
|
|
18340
|
+
function reconcileResume(view, observations = []) {
|
|
18341
|
+
if (observations.length > 0)
|
|
18342
|
+
return failure("permission_denied", "worker observations require native host verification");
|
|
18343
|
+
const candidate = captureCandidate(view.workspace.root, view.task.intent.data.scope, []);
|
|
18344
|
+
if (!candidate.ok)
|
|
18345
|
+
return candidate;
|
|
18346
|
+
const staleEvidenceIds = evaluateEvidence(view.task, candidate.data).filter((entry) => entry.status === "stale").map((entry) => entry.evidenceId);
|
|
18347
|
+
const blockers = [...view.task.progress.blockers];
|
|
18348
|
+
if (view.task.workers.some((entry) => isUncertainWorker(entry.data.state)))
|
|
18349
|
+
blockers.push({
|
|
18350
|
+
reason: "worker state requires reconciliation",
|
|
18351
|
+
dependentAction: "resume",
|
|
18352
|
+
refs: []
|
|
18353
|
+
});
|
|
18354
|
+
return success(null, null, {
|
|
18355
|
+
candidate: candidate.data,
|
|
18356
|
+
staleEvidenceIds,
|
|
18357
|
+
workerUpdates: [],
|
|
18358
|
+
blockers,
|
|
18359
|
+
reassessmentRequired: view.task.policy?.policyVersion !== POLICY_VERSION
|
|
18360
|
+
});
|
|
18361
|
+
}
|
|
18362
|
+
var exportDigest = (bundle) => sha256({
|
|
18363
|
+
schemaVersion: bundle.schemaVersion,
|
|
18364
|
+
exportedAt: bundle.exportedAt,
|
|
18365
|
+
sourceWorkspaceId: bundle.sourceWorkspaceId,
|
|
18366
|
+
task: bundle.task
|
|
18367
|
+
});
|
|
18199
18368
|
// packages/workit-core/src/core/external-action.ts
|
|
18200
18369
|
var externalActionSchema = discriminatedUnion("operation", [
|
|
18201
18370
|
object({
|
|
@@ -18229,6 +18398,13 @@ var externalActionSchema = discriminatedUnion("operation", [
|
|
|
18229
18398
|
babysit: boolean2().optional()
|
|
18230
18399
|
}).strict()
|
|
18231
18400
|
}).strict(),
|
|
18401
|
+
object({
|
|
18402
|
+
operation: literal("hosting.merge"),
|
|
18403
|
+
payload: object({
|
|
18404
|
+
target_branch: string2().min(1).optional(),
|
|
18405
|
+
source_branch: string2().min(1).optional()
|
|
18406
|
+
}).strict()
|
|
18407
|
+
}).strict(),
|
|
18232
18408
|
object({
|
|
18233
18409
|
operation: literal("youtrack.update"),
|
|
18234
18410
|
payload: object({
|
|
@@ -19342,7 +19518,10 @@ class WorkitCore {
|
|
|
19342
19518
|
observeDecision(request, observation) {
|
|
19343
19519
|
return this.recordDecision(request, observation, true);
|
|
19344
19520
|
}
|
|
19345
|
-
|
|
19521
|
+
observeStandingDecision(request) {
|
|
19522
|
+
return this.recordDecision(request, undefined, true, true);
|
|
19523
|
+
}
|
|
19524
|
+
recordDecision(request, nativeObservation, nativeRequired = false, standing = false) {
|
|
19346
19525
|
const root = this.contextRootError();
|
|
19347
19526
|
if (!root.ok)
|
|
19348
19527
|
return root;
|
|
@@ -19357,8 +19536,10 @@ class WorkitCore {
|
|
|
19357
19536
|
return failure("permission_denied", "stated choices cannot authorize mutating actions");
|
|
19358
19537
|
if (stated && !input.binding?.statedChoice)
|
|
19359
19538
|
return failure("invalid_input", "stated choices require binding.statedChoice");
|
|
19360
|
-
if (nativeRequired && nativeObservation === undefined && !stated)
|
|
19539
|
+
if (nativeRequired && nativeObservation === undefined && !stated && !standing)
|
|
19361
19540
|
return failure("permission_denied", "native decision observation is required");
|
|
19541
|
+
if (standing && (!input.binding?.standing || input.purpose !== "action" || input.response !== "approved"))
|
|
19542
|
+
return failure("invalid_input", "standing approvals need an approved action binding with a standing rule");
|
|
19362
19543
|
const task = this.store.readTask(input.taskId);
|
|
19363
19544
|
if (!task.ok)
|
|
19364
19545
|
return task;
|
|
@@ -19390,7 +19571,10 @@ class WorkitCore {
|
|
|
19390
19571
|
consumption: null
|
|
19391
19572
|
};
|
|
19392
19573
|
const data = { ...base, digest: decisionDigest(base) };
|
|
19393
|
-
const
|
|
19574
|
+
const standingApproval = standing && !stated ? verifyStandingApproval(this.store.root, task.data, this.context.caller, input.binding) : null;
|
|
19575
|
+
if (standingApproval && !standingApproval.ok)
|
|
19576
|
+
return standingApproval;
|
|
19577
|
+
const native = nativeRequired && !stated && !standingApproval ? verifyNativeDecision(this.context.nativeAuthority, {
|
|
19394
19578
|
observation: nativeObservation,
|
|
19395
19579
|
expected: {
|
|
19396
19580
|
taskId: task.data.id,
|
|
@@ -19421,7 +19605,7 @@ class WorkitCore {
|
|
|
19421
19605
|
const entry = {
|
|
19422
19606
|
id: newId(),
|
|
19423
19607
|
recordedAt: mutation.now,
|
|
19424
|
-
provenance: nativeProvenance ?? provenance(this.context, "agent_reported"),
|
|
19608
|
+
provenance: standingApproval?.ok === true ? standingApproval.data : nativeProvenance ?? provenance(this.context, "agent_reported"),
|
|
19425
19609
|
data
|
|
19426
19610
|
};
|
|
19427
19611
|
return success(mutation.revision, null, {
|
|
@@ -20567,7 +20751,7 @@ var SUPPORT_MATRIX = {
|
|
|
20567
20751
|
};
|
|
20568
20752
|
|
|
20569
20753
|
// packages/workit-mcp/src/server.ts
|
|
20570
|
-
import { existsSync as existsSync7, readFileSync as
|
|
20754
|
+
import { existsSync as existsSync7, readFileSync as readFileSync8, realpathSync as realpathSync7 } from "node:fs";
|
|
20571
20755
|
import path14 from "node:path";
|
|
20572
20756
|
|
|
20573
20757
|
// node_modules/@modelcontextprotocol/sdk/node_modules/zod/v4/core/core.js
|
|
@@ -26502,7 +26686,7 @@ var hostingCliAvailable = (provider) => whichOnPath(provider === "gitlab" ? "gla
|
|
|
26502
26686
|
|
|
26503
26687
|
// packages/workit-core/src/core/repo-context.ts
|
|
26504
26688
|
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
26505
|
-
import { existsSync as existsSync6, readFileSync as
|
|
26689
|
+
import { existsSync as existsSync6, readFileSync as readFileSync7, readdirSync as readdirSync4 } from "node:fs";
|
|
26506
26690
|
import path12 from "node:path";
|
|
26507
26691
|
var isSafeContextRange = (value) => value.length <= 4096 && (() => {
|
|
26508
26692
|
try {
|
|
@@ -26672,7 +26856,7 @@ var packageScripts = (cwd, keys) => {
|
|
|
26672
26856
|
if (!existsSync6(pkgPath))
|
|
26673
26857
|
return [];
|
|
26674
26858
|
try {
|
|
26675
|
-
const pkg = JSON.parse(
|
|
26859
|
+
const pkg = JSON.parse(readFileSync7(pkgPath, "utf8"));
|
|
26676
26860
|
return keys.filter((k) => pkg.scripts && typeof pkg.scripts[k] === "string").map((k) => `${k}: ${pkg.scripts?.[k]}`);
|
|
26677
26861
|
} catch {
|
|
26678
26862
|
return [];
|
|
@@ -26706,7 +26890,7 @@ var documentationFiles = (cwd) => {
|
|
|
26706
26890
|
var readTrimmed = (file, maxLines) => {
|
|
26707
26891
|
if (!existsSync6(file))
|
|
26708
26892
|
return "";
|
|
26709
|
-
const lines =
|
|
26893
|
+
const lines = readFileSync7(file, "utf8").split(`
|
|
26710
26894
|
`);
|
|
26711
26895
|
return lines.slice(0, maxLines).join(`
|
|
26712
26896
|
`);
|
|
@@ -26894,7 +27078,7 @@ function docsRefreshContext(root, range) {
|
|
|
26894
27078
|
const pkgPath = path12.join(cwd, "package.json");
|
|
26895
27079
|
if (existsSync6(pkgPath)) {
|
|
26896
27080
|
try {
|
|
26897
|
-
const pkg = JSON.parse(
|
|
27081
|
+
const pkg = JSON.parse(readFileSync7(pkgPath, "utf8"));
|
|
26898
27082
|
stdout += JSON.stringify({ name: pkg.name, version: pkg.version, scripts: pkg.scripts }, null, 2) + `
|
|
26899
27083
|
`;
|
|
26900
27084
|
} catch {}
|
|
@@ -27592,7 +27776,7 @@ var TOOL_CAPABILITIES = [
|
|
|
27592
27776
|
];
|
|
27593
27777
|
var VERSION = (() => {
|
|
27594
27778
|
try {
|
|
27595
|
-
const packageJson = JSON.parse(
|
|
27779
|
+
const packageJson = JSON.parse(readFileSync8(new URL("../package.json", import.meta.url), "utf8"));
|
|
27596
27780
|
return typeof packageJson.version === "string" ? packageJson.version : "0.0.0";
|
|
27597
27781
|
} catch {
|
|
27598
27782
|
return "0.0.0";
|