@intentius/chant 0.124.1 → 0.124.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/handlers/override-approve.d.ts +3 -1
- package/dist/cli/handlers/override-approve.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/mcp/workspace-tools.d.ts +5 -1
- package/dist/cli/mcp/workspace-tools.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +2 -0
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/lifecycle/gate-audit.d.ts +37 -13
- package/dist/lifecycle/gate-audit.d.ts.map +1 -1
- package/dist/lifecycle/gate-ledger.d.ts +1 -33
- package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
- package/dist/lifecycle/gate-origin.d.ts +14 -2
- package/dist/lifecycle/gate-origin.d.ts.map +1 -1
- package/dist/lifecycle/gate-pending.d.ts +41 -0
- package/dist/lifecycle/gate-pending.d.ts.map +1 -0
- package/dist/op/activities/activity-contracts.d.ts +1 -0
- package/dist/op/activities/activity-contracts.d.ts.map +1 -1
- package/dist/op/activities/decide.d.ts +2 -0
- package/dist/op/activities/decide.d.ts.map +1 -1
- package/dist/op/gate-point-run.d.ts +5 -77
- package/dist/op/gate-point-run.d.ts.map +1 -1
- package/dist/op/gate-point.d.ts +18 -0
- package/dist/op/gate-point.d.ts.map +1 -1
- package/dist/op/gate.d.ts +8 -0
- package/dist/op/gate.d.ts.map +1 -1
- package/dist/op/index.d.ts +4 -4
- package/dist/op/index.d.ts.map +1 -1
- package/dist/op/op-waves-run.d.ts +37 -6
- package/dist/op/op-waves-run.d.ts.map +1 -1
- package/dist/op/op-waves.d.ts +38 -4
- package/dist/op/op-waves.d.ts.map +1 -1
- package/dist/op/steward-points.d.ts +7 -0
- package/dist/op/steward-points.d.ts.map +1 -1
- package/dist/override-gate.d.ts +8 -2
- package/dist/override-gate.d.ts.map +1 -1
- package/dist/point-decision.d.ts +134 -0
- package/dist/point-decision.d.ts.map +1 -0
- package/dist/pr-forge.d.ts +37 -2
- package/dist/pr-forge.d.ts.map +1 -1
- package/dist/workspace/decide.d.ts +14 -2
- package/dist/workspace/decide.d.ts.map +1 -1
- package/dist/workspace/reason-codes.d.ts +2 -1
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/cli/handlers/override-approve.test.ts +16 -0
- package/src/cli/handlers/override-approve.ts +6 -2
- package/src/cli/main.test.ts +3 -0
- package/src/cli/main.ts +9 -2
- package/src/cli/mcp/workspace-tools.ts +7 -3
- package/src/cli/registry.ts +2 -0
- package/src/lifecycle/gate-audit.test.ts +42 -0
- package/src/lifecycle/gate-audit.ts +102 -34
- package/src/lifecycle/gate-ledger.ts +3 -53
- package/src/lifecycle/gate-origin.ts +20 -3
- package/src/lifecycle/gate-pending.ts +62 -0
- package/src/op/activities/activity-contracts.ts +1 -0
- package/src/op/activities/decide.ts +3 -0
- package/src/op/gate-point-ci.e2e.test.ts +171 -0
- package/src/op/gate-point-mcp.e2e.test.ts +163 -0
- package/src/op/gate-point-run.ts +18 -149
- package/src/op/gate-point.e2e.test.ts +2 -0
- package/src/op/gate-point.test.ts +1 -1
- package/src/op/gate-point.ts +27 -3
- package/src/op/gate.ts +10 -0
- package/src/op/index.ts +10 -1
- package/src/op/op-waves-run.ts +84 -19
- package/src/op/op-waves.test.ts +96 -0
- package/src/op/op-waves.ts +60 -4
- package/src/op/steward-points.ts +8 -0
- package/src/override-gate.test.ts +5 -0
- package/src/override-gate.ts +8 -2
- package/src/point-decision-bundle.test.ts +62 -0
- package/src/point-decision.e2e.test.ts +95 -0
- package/src/point-decision.ts +280 -0
- package/src/pr-forge.test.ts +62 -0
- package/src/pr-forge.ts +60 -9
- package/src/workspace/decide.ts +92 -8
- package/src/workspace/points-cli.ts +1 -1
- package/src/workspace/points-write.schema.json +1 -0
- package/src/workspace/reason-codes.ts +2 -1
package/src/workspace/decide.ts
CHANGED
|
@@ -93,6 +93,7 @@ import { IdentityError, refuseUnidentified } from "./identity";
|
|
|
93
93
|
import { scopeSource } from "./write-scope";
|
|
94
94
|
import { RefCASConflictError } from "../lifecycle/git";
|
|
95
95
|
import { readLedgerAnswers, refreshLedger, withLedgerAnswers, writeLedgerAnswer, type LedgerAnswers } from "./answers-ledger";
|
|
96
|
+
import { currentGateOrigin, isModelAuthored, type GateOrigin } from "../lifecycle/gate-origin";
|
|
96
97
|
|
|
97
98
|
/** The version of the write documents `points ask`, `points answer` and `points retract` print. */
|
|
98
99
|
export const POINTS_WRITE_CONTRACT_VERSION = 1;
|
|
@@ -122,6 +123,8 @@ export const POINTS_WRITE_ERROR_CODES = [
|
|
|
122
123
|
"record-id-taken",
|
|
123
124
|
/** The answer was given during a steward's turn (#2749): a steward never answers a question, its own or another's. */
|
|
124
125
|
"answer-in-steward-turn",
|
|
126
|
+
/** The answer or retraction came over MCP or ACP, and the question constrains a gate that is never resolved there (#3817). */
|
|
127
|
+
"answer-never-over-mcp",
|
|
125
128
|
/** An answerer is named by a bare name, and identity.attribution at base is identified (#3163). */
|
|
126
129
|
"principal-unidentified",
|
|
127
130
|
/** `points retract` names a question that has no answer: escalated or proposed (#3351). */
|
|
@@ -442,7 +445,9 @@ export interface AskPointOptions {
|
|
|
442
445
|
* or the lifecycle ledger (#2786). Defaults to the ledger in a steward's
|
|
443
446
|
* turn (`steward` given, or this process a steward's turn), since a steward
|
|
444
447
|
* never writes the checkout, and to the tree otherwise. A question already
|
|
445
|
-
* held on the ledger is rewritten there whatever this says.
|
|
448
|
+
* held on the ledger is rewritten there whatever this says. Outside a
|
|
449
|
+
* steward's turn, `points ask --store ledger` and a gate's `point.store`
|
|
450
|
+
* reach it (#3815), for a checkout that does not outlive the run.
|
|
446
451
|
*/
|
|
447
452
|
store?: "tree" | "ledger";
|
|
448
453
|
/** The date written as asked_on, and answered_on for a table's answer, YYYY-MM-DD. Defaults to today, in UTC. */
|
|
@@ -715,6 +720,13 @@ export interface AnswerPointOptions {
|
|
|
715
720
|
kind?: string;
|
|
716
721
|
on?: string;
|
|
717
722
|
dryRun?: boolean;
|
|
723
|
+
/**
|
|
724
|
+
* The channel the answer came through (#3817). Defaults to this process's
|
|
725
|
+
* (`currentGateOrigin`). Over MCP or ACP nobody `by` names counts toward
|
|
726
|
+
* the quorum, and a question that constrains a `neverOverMcp` gate is
|
|
727
|
+
* refused outright.
|
|
728
|
+
*/
|
|
729
|
+
origin?: GateOrigin;
|
|
718
730
|
}
|
|
719
731
|
|
|
720
732
|
/**
|
|
@@ -813,7 +825,10 @@ async function findQuestion(cwd: string, kind: string | undefined, id: string):
|
|
|
813
825
|
if (kinds.length === 0) throw new PointsWriteError("points-undeclared", "no record kind with an answers block is declared: name one with --kind, or declare one in chant.workspace.json");
|
|
814
826
|
for (const k of kinds) {
|
|
815
827
|
const opened = await openAnswers(k, cwd);
|
|
816
|
-
// A question a steward asked is on the ledger (#2786), and people's answer goes back there.
|
|
828
|
+
// A question a steward asked is on the ledger (#2786), and people's answer goes back there. So is
|
|
829
|
+
// one a CI job asked with --store ledger (#3815), from a checkout that is gone: fetch the
|
|
830
|
+
// branch first, so a clone that never ran the job finds the question.
|
|
831
|
+
await refreshLedger(opened.o.loaded.file);
|
|
817
832
|
const read = await readWithLedger(opened.o);
|
|
818
833
|
const target = read.before.find((e) => e.id === id);
|
|
819
834
|
if (target && target.data !== null) return { opened, before: read.before, read, target: target as FoundQuestion["target"] };
|
|
@@ -821,20 +836,83 @@ async function findQuestion(cwd: string, kind: string | undefined, id: string):
|
|
|
821
836
|
throw new PointsWriteError("record-not-found", `no answer record has id ${id}`);
|
|
822
837
|
}
|
|
823
838
|
|
|
824
|
-
/**
|
|
825
|
-
|
|
826
|
-
|
|
839
|
+
/** A model-authored channel's name, as a person reads it. */
|
|
840
|
+
const channelName = (origin: GateOrigin): string => (origin === "acp" ? "ACP" : "MCP");
|
|
841
|
+
|
|
842
|
+
/**
|
|
843
|
+
* Count `by` toward the point's quorum, or refuse with `quorum-not-met`.
|
|
844
|
+
*
|
|
845
|
+
* Over MCP or ACP (#3817) the names are the model's to choose, as a gate's
|
|
846
|
+
* approver is (`lifecycle/gate-origin.ts`), so the answer is an agent's and
|
|
847
|
+
* nobody it names counts: a person answers at a shell or through hud.
|
|
848
|
+
*/
|
|
849
|
+
function meetQuorum(name: string, by: string[], quorum: { count: number; roles?: string[] }, root: string, steward: string | undefined, verb: "answer" | "retract", origin: GateOrigin): { counted: string[] } {
|
|
850
|
+
const overModel = isModelAuthored(origin);
|
|
851
|
+
const tally = overModel
|
|
852
|
+
? { counted: [] as string[], left: by.map((n) => ({ name: n, why: `was named over ${channelName(origin)}, where an answer is an agent's` })), met: false }
|
|
853
|
+
: tallyQuorum(by, quorum, policyFor(root), steward);
|
|
827
854
|
if (!tally.met) {
|
|
828
855
|
const got = tally.counted.length;
|
|
829
856
|
const left = tally.left.length ? `; not counted: ${tally.left.map((l) => `${l.name}, who ${l.why}`).join("; ")}` : "";
|
|
857
|
+
const remedy = overModel ? `: a person ${verb === "answer" ? "answers" : "retracts it"} at a shell with \`chant workspace points ${verb}\`, or through hud` : "";
|
|
830
858
|
throw new PointsWriteError(
|
|
831
859
|
"quorum-not-met",
|
|
832
|
-
`${name} needs ${quorum.count} ${quorum.count === 1 ? "person" : "people"}${quorum.roles ? ` holding ${quorum.roles.join(" or ")}` : ""} to ${verb === "answer" ? "answer" : "retract an answer"}, and ${got} ${got === 1 ? "counts" : "count"}${got ? ` (${tally.counted.join(", ")})` : ""}${left}`,
|
|
860
|
+
`${name} needs ${quorum.count} ${quorum.count === 1 ? "person" : "people"}${quorum.roles ? ` holding ${quorum.roles.join(" or ")}` : ""} to ${verb === "answer" ? "answer" : "retract an answer"}, and ${got} ${got === 1 ? "counts" : "count"}${got ? ` (${tally.counted.join(", ")})` : ""}${left}${remedy}`,
|
|
833
861
|
);
|
|
834
862
|
}
|
|
835
863
|
return tally;
|
|
836
864
|
}
|
|
837
865
|
|
|
866
|
+
/** The gates a question's `constrains` names, as `gate:<op>/<gate>` with an optional `@<env>` (`op/gate-point.ts`'s `gateSubject`). */
|
|
867
|
+
function constrainedGates(d: Record<string, unknown>): { op: string; gate: string }[] {
|
|
868
|
+
const out: { op: string; gate: string }[] = [];
|
|
869
|
+
for (const c of Array.isArray(d.constrains) ? d.constrains : []) {
|
|
870
|
+
if (typeof c !== "string" || !c.startsWith("gate:")) continue;
|
|
871
|
+
const rest = c.slice("gate:".length);
|
|
872
|
+
const at = rest.lastIndexOf("@");
|
|
873
|
+
const named = at > 0 ? rest.slice(0, at) : rest;
|
|
874
|
+
const slash = named.indexOf("/");
|
|
875
|
+
if (slash > 0 && slash < named.length - 1) out.push({ op: named.slice(0, slash), gate: named.slice(slash + 1) });
|
|
876
|
+
}
|
|
877
|
+
return out;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
/**
|
|
881
|
+
* Refuse an answer or retraction over MCP or ACP to a question that
|
|
882
|
+
* constrains a gate never resolved there (#3817, #3485): the gate-only op
|
|
883
|
+
* names chant knows, a pending fact on the gate ledger that says so (as
|
|
884
|
+
* terragucci's records do), or the Op's own `neverOverMcp`. `op-approve`
|
|
885
|
+
* refuses the same gates.
|
|
886
|
+
*/
|
|
887
|
+
async function refuseNeverOverMcp(id: string, d: Record<string, unknown>, root: string, origin: GateOrigin, does: "answers" | "retracts"): Promise<void> {
|
|
888
|
+
if (!isModelAuthored(origin)) return;
|
|
889
|
+
const gates = constrainedGates(d);
|
|
890
|
+
if (gates.length === 0) return;
|
|
891
|
+
const { isNeverOverMcpGateOp } = await import("../op/gate-name");
|
|
892
|
+
const { readGateLedger } = await import("../lifecycle/gate-ledger");
|
|
893
|
+
let configs: Map<string, { config: { neverOverMcp?: boolean } }> | undefined;
|
|
894
|
+
for (const { op, gate } of gates) {
|
|
895
|
+
let never = isNeverOverMcpGateOp(op);
|
|
896
|
+
if (!never) {
|
|
897
|
+
const { pending } = await readGateLedger(op, { cwd: root }).catch(() => ({ pending: [] as { gate: string; neverOverMcp?: true }[] }));
|
|
898
|
+
never = pending.some((p) => p.gate === gate && p.neverOverMcp === true);
|
|
899
|
+
}
|
|
900
|
+
if (!never) {
|
|
901
|
+
if (configs === undefined) {
|
|
902
|
+
const { discoverOps } = await import("../op/discover");
|
|
903
|
+
configs = await discoverOps({ cwd: root }).then((r) => r.ops as Map<string, { config: { neverOverMcp?: boolean } }>).catch(() => new Map());
|
|
904
|
+
}
|
|
905
|
+
never = Boolean(configs.get(op)?.config.neverOverMcp);
|
|
906
|
+
}
|
|
907
|
+
if (never) {
|
|
908
|
+
throw new PointsWriteError(
|
|
909
|
+
"answer-never-over-mcp",
|
|
910
|
+
`${id} constrains gate "${gate}" on "${op}", which is never resolved over ${channelName(origin)}, so its question is not ${does === "answers" ? "answered" : "retracted"} there either: a person ${does === "answers" ? "answers" : "retracts the answer to"} it at a shell with \`chant workspace points ${does === "answers" ? "answer" : "retract"} ${id}\`, or through hud`,
|
|
911
|
+
);
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
|
|
838
916
|
/**
|
|
839
917
|
* People answer an open question: `points answer`. A proposed question the
|
|
840
918
|
* people answer as the model did is confirmed, and keeps the model as its
|
|
@@ -884,8 +962,10 @@ async function answerPointLocked(opts: AnswerPointOptions): Promise<PointsWriteD
|
|
|
884
962
|
if (value === undefined || !allowed.includes(value)) {
|
|
885
963
|
throw new PointsWriteError("answer-not-candidate", `${opts.id} takes one of ${allowed.map((c) => JSON.stringify(c)).join(", ")}, not ${JSON.stringify(opts.answer)}`);
|
|
886
964
|
}
|
|
965
|
+
const origin = opts.origin ?? currentGateOrigin();
|
|
966
|
+
await refuseNeverOverMcp(opts.id, d, o.root, origin, "answers");
|
|
887
967
|
const quorum = quorumOf(point);
|
|
888
|
-
const tally = meetQuorum(name, opts.by, quorum, o.root, askedByOf(d)?.steward, "answer");
|
|
968
|
+
const tally = meetQuorum(name, opts.by, quorum, o.root, askedByOf(d)?.steward, "answer", origin);
|
|
889
969
|
const on = opts.on ?? today();
|
|
890
970
|
const decider = d.decider as Record<string, unknown>;
|
|
891
971
|
const confirmed = target.state === "proposed" && decider.kind === "model" && d.answer === value;
|
|
@@ -960,6 +1040,8 @@ export interface RetractAnswerOptions {
|
|
|
960
1040
|
kind?: string;
|
|
961
1041
|
on?: string;
|
|
962
1042
|
dryRun?: boolean;
|
|
1043
|
+
/** The channel the retraction came through, counted as for an answer (#3817). */
|
|
1044
|
+
origin?: GateOrigin;
|
|
963
1045
|
}
|
|
964
1046
|
|
|
965
1047
|
/**
|
|
@@ -998,8 +1080,10 @@ async function retractAnswerLocked(opts: RetractAnswerOptions): Promise<PointsWr
|
|
|
998
1080
|
const name = String(d.point);
|
|
999
1081
|
const point = pointOf(opened.points, name);
|
|
1000
1082
|
if (!point) throw new PointsWriteError("point-unknown", `${opts.id} answers ${name}, which ${opened.pointsFile} no longer declares, so no quorum can take the answer back: ask the point as it is declared now`);
|
|
1083
|
+
const origin = opts.origin ?? currentGateOrigin();
|
|
1084
|
+
await refuseNeverOverMcp(opts.id, d, o.root, origin, "retracts");
|
|
1001
1085
|
const quorum = quorumOf(point);
|
|
1002
|
-
const tally = meetQuorum(name, opts.by, quorum, o.root, askedByOf(d)?.steward, "retract");
|
|
1086
|
+
const tally = meetQuorum(name, opts.by, quorum, o.root, askedByOf(d)?.steward, "retract", origin);
|
|
1003
1087
|
const on = opts.on ?? today();
|
|
1004
1088
|
const type = String(d.question_type);
|
|
1005
1089
|
const decider = d.decider !== null && typeof d.decider === "object" && !Array.isArray(d.decider) ? (d.decider as Record<string, unknown>) : { kind: "quorum" };
|
|
@@ -304,7 +304,7 @@ export async function runWorkspacePoints(ctx: CommandContext): Promise<number> {
|
|
|
304
304
|
if (!("value" in response)) return print(response);
|
|
305
305
|
ask = responseAsk(response.value);
|
|
306
306
|
}
|
|
307
|
-
return print(await askPoint({ cwd, point, inputs: inputs.value, candidates: given, subject: args.subject, kind: args.kind, ask, dryRun: args.dryRun }));
|
|
307
|
+
return print(await askPoint({ cwd, point, inputs: inputs.value, candidates: given, subject: args.subject, kind: args.kind, ask, ...(args.store ? { store: args.store } : {}), dryRun: args.dryRun }));
|
|
308
308
|
}
|
|
309
309
|
if (verb === "answer") {
|
|
310
310
|
const id = args.extraPositional2;
|
|
@@ -157,8 +157,9 @@ export const REASONS = {
|
|
|
157
157
|
"point-inputs-invalid": "The inputs given to an ask are not a JSON object of the point's declared inputs.",
|
|
158
158
|
"point-decider-failed": "A model decider that fails closed (unreachable: fail) could not answer, so the ask wrote nothing.",
|
|
159
159
|
"answer-not-candidate": "The people's answer is not one of the question's candidates.",
|
|
160
|
-
"quorum-not-met": "Too few of the people who answered count toward the point's quorum: distinct, not holding the agent role, not the steward that asked, and holding one of its roles when it names any.",
|
|
160
|
+
"quorum-not-met": "Too few of the people who answered count toward the point's quorum: distinct, not holding the agent role, not the steward that asked, and holding one of its roles when it names any. Nobody named over MCP or ACP counts (#3817).",
|
|
161
161
|
"answer-in-steward-turn": "The answer was given during a steward's turn, or by a process it started: a steward never answers a decision point, and a person answers it through hud or at a shell.",
|
|
162
|
+
"answer-never-over-mcp": "The answer or retraction came over MCP or ACP, or from a process a session on one started, and the question constrains a gate that is never resolved there: a person answers it at a shell or through hud (#3817).",
|
|
162
163
|
"answer-not-answered": "points retract names a question that has no answer to retract: it is escalated or proposed, and people answer it instead (#3351).",
|
|
163
164
|
"answer-field-unsupported": "The answer kind's copy of point-answer.schema.json predates a field the write needs, a note, a relay, a retraction or an ad-hoc question's asked: copy the schema anew to turn it on (#3351, #3402, #3403).",
|
|
164
165
|
"point-candidates-invalid": "points ask names an ad-hoc point without --candidates, a declared point with them, or candidates that are not a question and criteria of the point's question type (#3403).",
|