@intentius/chant 0.124.5 → 0.124.6
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/gate-resume.d.ts +32 -1
- package/dist/cli/handlers/gate-resume.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/lifecycle/gate-ledger.d.ts +7 -0
- package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
- package/dist/op/gate-point-run.d.ts +1 -1
- package/dist/op/gate-point-run.d.ts.map +1 -1
- package/dist/op/gate-point.d.ts +6 -0
- package/dist/op/gate-point.d.ts.map +1 -1
- package/dist/op/gate-resume.d.ts +2 -31
- package/dist/op/gate-resume.d.ts.map +1 -1
- package/dist/op/gate-run-locator.d.ts +40 -0
- package/dist/op/gate-run-locator.d.ts.map +1 -0
- package/dist/point-decision.d.ts +63 -1
- package/dist/point-decision.d.ts.map +1 -1
- package/dist/workspace/points-cli.d.ts.map +1 -1
- package/dist/workspace/records-cli.d.ts +15 -0
- package/dist/workspace/records-cli.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/cli/handlers/gate-resume.ts +84 -11
- package/src/cli/main.ts +4 -2
- package/src/lifecycle/gate-ledger.ts +7 -0
- package/src/op/gate-point-ci.e2e.test.ts +39 -2
- package/src/op/gate-point-resume.e2e.test.ts +203 -0
- package/src/op/gate-point-run.ts +3 -0
- package/src/op/gate-point.ts +11 -0
- package/src/op/gate-resume.ts +5 -95
- package/src/op/gate-run-locator.ts +105 -0
- package/src/point-decision-bundle.test.ts +1 -0
- package/src/point-decision.ts +97 -1
- package/src/workspace/points-cli.ts +7 -2
- package/src/workspace/records-cli.ts +38 -7
- package/src/workspace/records.schema.json +4 -0
package/src/point-decision.ts
CHANGED
|
@@ -20,6 +20,11 @@
|
|
|
20
20
|
* TypeScript, zod or git code, which `point-decision-bundle.test.ts` holds
|
|
21
21
|
* it to. `./op/gate-point-run.ts` re-exports it, and chant's own runs decide
|
|
22
22
|
* by it.
|
|
23
|
+
*
|
|
24
|
+
* A gate's resolution is written only when its run asks again, so a resume
|
|
25
|
+
* job asks {@link pointGateResumable} whether a waiting job's question now
|
|
26
|
+
* has an answer that passes the gate (#3835), with the questions
|
|
27
|
+
* {@link chantPointQuestions} reads. `chant run resume` makes the same check.
|
|
23
28
|
*/
|
|
24
29
|
|
|
25
30
|
import { execFile } from "node:child_process";
|
|
@@ -31,6 +36,7 @@ import { parseDuration } from "./op/duration";
|
|
|
31
36
|
import type { GateLedgerPort } from "./op/gate";
|
|
32
37
|
import { DEFAULT_GATE_POINT_PASS, defaultGatePointStore, gatePointInputs, gateSubject, type GatePoint, type GatePointFacts, type GatePointStore } from "./op/gate-point";
|
|
33
38
|
import type { WaitingPoint } from "./op/steward-points";
|
|
39
|
+
import { resolveGateRunLocator, type GateRunLocator } from "./op/gate-run-locator";
|
|
34
40
|
|
|
35
41
|
export {
|
|
36
42
|
DEFAULT_GATE_POINT_PASS,
|
|
@@ -40,12 +46,14 @@ export {
|
|
|
40
46
|
gatePointOf,
|
|
41
47
|
gatePointProblems,
|
|
42
48
|
gateSubject,
|
|
49
|
+
parseGateSubject,
|
|
43
50
|
type GatePoint,
|
|
44
51
|
type GatePointFacts,
|
|
45
52
|
type GatePointStore,
|
|
46
53
|
} from "./op/gate-point";
|
|
47
54
|
export type { GateAnswerRef, GateResolutionRecord, GateResolutionInput, PendingGateInput, PendingGateRecord } from "./lifecycle/gate-ledger";
|
|
48
55
|
export type { WaitingPoint } from "./op/steward-points";
|
|
56
|
+
export { isGateRunLocator, resolveGateRunLocator, type GateRunLocator } from "./op/gate-run-locator";
|
|
49
57
|
|
|
50
58
|
/**
|
|
51
59
|
* The gate ledger as {@link evaluatePointGate} uses it: read an op's facts,
|
|
@@ -107,6 +115,12 @@ export interface PointGateInput {
|
|
|
107
115
|
/** The environment the run was started for: the `gate.env` input. Op gate facts on the ledger record none. */
|
|
108
116
|
env?: string;
|
|
109
117
|
now?: string;
|
|
118
|
+
/**
|
|
119
|
+
* Where this job runs, for `chant run resume` to start it again once the
|
|
120
|
+
* question is answered (#3835). Default: read from the CI environment
|
|
121
|
+
* ({@link resolveGateRunLocator}); false records none.
|
|
122
|
+
*/
|
|
123
|
+
resume?: GateRunLocator | false;
|
|
110
124
|
}
|
|
111
125
|
|
|
112
126
|
/** What reaching a gate that asks a point decided. */
|
|
@@ -188,6 +202,7 @@ export async function evaluatePointGate(port: PointGateLedgerPort, asker: GatePo
|
|
|
188
202
|
return { satisfied: false, waiting, pending: standing, recorded: false };
|
|
189
203
|
}
|
|
190
204
|
const url = resolveApprovalUrl();
|
|
205
|
+
const resume = input.resume === false ? undefined : (input.resume ?? resolveGateRunLocator());
|
|
191
206
|
const { record, pushed, pushWarning } = await port.appendPending({
|
|
192
207
|
op: input.op,
|
|
193
208
|
gate: input.gate,
|
|
@@ -197,11 +212,77 @@ export async function evaluatePointGate(port: PointGateLedgerPort, asker: GatePo
|
|
|
197
212
|
...(input.runId ? { runId: input.runId } : {}),
|
|
198
213
|
...(url ? { url } : {}),
|
|
199
214
|
...(input.planDigest !== undefined ? { planDigest: input.planDigest } : {}),
|
|
200
|
-
|
|
215
|
+
...(resume ? { resume } : {}),
|
|
216
|
+
answer: { ...ref, ...(input.point.pass ? { pass: input.point.pass } : {}) },
|
|
201
217
|
});
|
|
202
218
|
return { satisfied: false, waiting, pending: record, recorded: true, pushed, ...(pushWarning ? { pushWarning } : {}) };
|
|
203
219
|
}
|
|
204
220
|
|
|
221
|
+
/**
|
|
222
|
+
* A question's state as a resume reads it: what `chant workspace points
|
|
223
|
+
* --json` lists for it. {@link chantPointQuestions} reads them through the
|
|
224
|
+
* chant bin; chant's own resume reads the workspace.
|
|
225
|
+
*/
|
|
226
|
+
export interface PointQuestionState {
|
|
227
|
+
id: string;
|
|
228
|
+
state: "escalated" | "proposed" | "answered";
|
|
229
|
+
/** The answer, or null while the question is open. */
|
|
230
|
+
answer: string | boolean | null;
|
|
231
|
+
answeredBy: string[];
|
|
232
|
+
/** When it was answered, as the record says, or null. */
|
|
233
|
+
answeredOn: string | null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Whether a waiting point-asking gate can be resumed, and why not. */
|
|
237
|
+
export type PointResumeCheck =
|
|
238
|
+
| {
|
|
239
|
+
resumable: true;
|
|
240
|
+
/** The answer that passes the gate, as the resumed run will cite it. */
|
|
241
|
+
answer: GateAnswerRef;
|
|
242
|
+
/** Who answered: the people, joined, or `quorum` when the record names none. */
|
|
243
|
+
answeredBy: string;
|
|
244
|
+
/** When it was answered, or null. A resume reads a run started after it as already resumed. */
|
|
245
|
+
answeredOn: string | null;
|
|
246
|
+
}
|
|
247
|
+
| {
|
|
248
|
+
resumable: false;
|
|
249
|
+
/** `open`: nobody has answered; `refused`: the answer does not pass the gate; `unknown`: no question with the id was read; `not-a-point`: the pending fact cites no question. */
|
|
250
|
+
why: "open" | "refused" | "unknown" | "not-a-point";
|
|
251
|
+
message: string;
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Whether the job behind `standing`, a pending fact a gate that asks a
|
|
256
|
+
* decision point recorded, can be started again (#3835): its question is
|
|
257
|
+
* answered with an answer the gate passes on (the pending fact's
|
|
258
|
+
* `answer.pass`, default a noul point's yes). A gate only writes its
|
|
259
|
+
* resolution when its run asks the point again, so nothing on the gate
|
|
260
|
+
* ledger says an answered question passes; this is the check
|
|
261
|
+
* `chant run resume` makes, and a product's own resume job makes the same
|
|
262
|
+
* call with the questions it read (`chantPointQuestions`). It approves
|
|
263
|
+
* nothing: the resumed run asks again and decides.
|
|
264
|
+
*/
|
|
265
|
+
export function pointGateResumable(standing: Pick<PendingGateRecord, "answer">, question: PointQuestionState | undefined): PointResumeCheck {
|
|
266
|
+
const ref = standing.answer;
|
|
267
|
+
if (!ref) return { resumable: false, why: "not-a-point", message: "the pending fact cites no decision point's question" };
|
|
268
|
+
if (!question || question.id !== ref.id) return { resumable: false, why: "unknown", message: `question ${ref.id} (${ref.point}) can't be read here` };
|
|
269
|
+
if (question.state !== "answered" || question.answer === null) return { resumable: false, why: "open", message: `question ${ref.id} (${ref.point}) is ${question.state}, not answered yet` };
|
|
270
|
+
const pass = ref.pass ?? DEFAULT_GATE_POINT_PASS;
|
|
271
|
+
if (!pass.includes(question.answer)) {
|
|
272
|
+
return {
|
|
273
|
+
resumable: false,
|
|
274
|
+
why: "refused",
|
|
275
|
+
message: `question ${ref.id} (${ref.point}) was answered ${JSON.stringify(question.answer)}, and the gate passes only on ${pass.map((a) => JSON.stringify(a)).join(" or ")}`,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
return {
|
|
279
|
+
resumable: true,
|
|
280
|
+
answer: { point: ref.point, id: ref.id, path: ref.path, answer: question.answer, ...(question.answeredBy.length > 0 ? { answeredBy: question.answeredBy } : {}) },
|
|
281
|
+
answeredBy: question.answeredBy.length > 0 ? question.answeredBy.join(", ") : "quorum",
|
|
282
|
+
answeredOn: question.answeredOn,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
205
286
|
/** How {@link chantGatePointAsker} runs chant. */
|
|
206
287
|
export interface ChantGatePointAskerOptions {
|
|
207
288
|
/** The chant command and its leading arguments, such as `["npx", "chant"]` or `[process.execPath, "/path/to/chant"]`. */
|
|
@@ -278,3 +359,18 @@ export function chantGatePointAsker(opts: ChantGatePointAskerOptions): GatePoint
|
|
|
278
359
|
},
|
|
279
360
|
};
|
|
280
361
|
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Every question the workspace lists, by id, read through the chant bin with
|
|
365
|
+
* `chant workspace points --json` (the questions held on `chant/lifecycle`
|
|
366
|
+
* included), for {@link pointGateResumable} in a tool that bundles this
|
|
367
|
+
* module (#3835). Fetch `chant/lifecycle` first, as reading the gate ledger
|
|
368
|
+
* does, so an answer given from another clone is seen.
|
|
369
|
+
*/
|
|
370
|
+
export async function chantPointQuestions(opts: ChantGatePointAskerOptions): Promise<Map<string, PointQuestionState>> {
|
|
371
|
+
const listed = await runChantJson(opts, ["workspace", "points", "--json"]);
|
|
372
|
+
const bad = refusal("chant workspace points", listed);
|
|
373
|
+
if (bad) throw bad;
|
|
374
|
+
const questions = (listed.questions as PointQuestionState[] | undefined) ?? [];
|
|
375
|
+
return new Map(questions.map((q) => [q.id, { id: q.id, state: q.state, answer: q.answer ?? null, answeredBy: q.answeredBy ?? [], answeredOn: q.answeredOn ?? null }]));
|
|
376
|
+
}
|
|
@@ -250,7 +250,7 @@ export function responseAsk(response: unknown): ModelAsk {
|
|
|
250
250
|
const USAGE = [
|
|
251
251
|
"chant workspace points [--open] [--kind <kind file>] [--at <rev>] [--json]",
|
|
252
252
|
"chant workspace points ask <point> --inputs <file|-|json> [--candidates <file|-|json>] [--response <file>] [--subject <id>] [--kind <kind file>] [--dry-run]",
|
|
253
|
-
"chant workspace points answer <id> --answer <value> --by <name> [--by <name>...] [--note <text>] [--relayed-by <principal>] [--kind <kind file>] [--dry-run]",
|
|
253
|
+
"chant workspace points answer <id> --answer <value> --by <name> [--by <name>...] [--note <text>] [--relayed-by <principal>] [--kind <kind file>] [--resume] [--dry-run]",
|
|
254
254
|
"chant workspace points retract <id> --by <name> [--by <name>...] [--note <text>] [--kind <kind file>] [--dry-run]",
|
|
255
255
|
].join("\n");
|
|
256
256
|
|
|
@@ -311,7 +311,12 @@ export async function runWorkspacePoints(ctx: CommandContext): Promise<number> {
|
|
|
311
311
|
if (!id) return print(usage("answer", "answer needs the question's id"));
|
|
312
312
|
if (args.answer === undefined) return print(usage("answer", "--answer <value> is required"));
|
|
313
313
|
if (!args.bys || args.bys.length === 0) return print(usage("answer", "--by <name> is required, once for each person who answered"));
|
|
314
|
-
|
|
314
|
+
const answered = await answerPoint({ cwd, id, answer: args.answer, by: args.bys, note: args.note, relayedBy: args.relayedBy, kind: args.kind, dryRun: args.dryRun });
|
|
315
|
+
const code = print(answered);
|
|
316
|
+
// --resume (#3835): start the job waiting at the gate the question names, as `chant approve --resume` does.
|
|
317
|
+
if (code !== 0 || args.dryRun || !(args.resumeRun || args.resume !== undefined) || !("question" in answered)) return code;
|
|
318
|
+
const { resumeAfterAnswer } = await import("../cli/handlers/gate-resume");
|
|
319
|
+
return resumeAfterAnswer(answered.question, cwd);
|
|
315
320
|
}
|
|
316
321
|
if (verb === "retract") {
|
|
317
322
|
const id = args.extraPositional2;
|
|
@@ -99,6 +99,13 @@ export interface RecordsQuery {
|
|
|
99
99
|
workGaps?: boolean;
|
|
100
100
|
/** List only the records that are modified or new against `HEAD` (#3160). Needs git and a working-tree read. */
|
|
101
101
|
uncommitted?: boolean;
|
|
102
|
+
/**
|
|
103
|
+
* For an answer kind read in the working tree (#3833): fetch `chant/lifecycle`
|
|
104
|
+
* first, best effort, as `points answer` does, so an answer held there by
|
|
105
|
+
* another clone or a CI job is listed. The command sets it; other readers
|
|
106
|
+
* read the local ledger as it is.
|
|
107
|
+
*/
|
|
108
|
+
fetchLedger?: boolean;
|
|
102
109
|
}
|
|
103
110
|
|
|
104
111
|
/**
|
|
@@ -146,6 +153,14 @@ export type RecordView = RecordEntry & {
|
|
|
146
153
|
* steward keeps on the lifecycle ledger (#2786) laid over it.
|
|
147
154
|
*/
|
|
148
155
|
answers?: WorkAnswer[];
|
|
156
|
+
/**
|
|
157
|
+
* For a record of an answer kind (one with an `answers` block) read in the
|
|
158
|
+
* working tree (#3833): where the record is held on the lifecycle ledger,
|
|
159
|
+
* `chant/lifecycle:<path>`, as `points` names it for a question, or null
|
|
160
|
+
* when it is the working tree's file. A record held on the ledger has no
|
|
161
|
+
* `worktree` or `lastWrite`, since the checkout doesn't hold it.
|
|
162
|
+
*/
|
|
163
|
+
ledger?: string | null;
|
|
149
164
|
};
|
|
150
165
|
|
|
151
166
|
/** The role in the trust policy whose holders' verdicts the quorum does not count (#2671). */
|
|
@@ -398,7 +413,19 @@ export async function queryRecords(query: RecordsQuery): Promise<RecordsDocument
|
|
|
398
413
|
// A revision has no working tree to hold anything uncommitted; the CLI and the MCP tool refuse the pair first.
|
|
399
414
|
if (query.uncommitted && query.at !== undefined) throw new Error("--uncommitted reads the working tree and takes no --at");
|
|
400
415
|
try {
|
|
401
|
-
|
|
416
|
+
// An answer kind's records include the answers held on the lifecycle ledger (#2786, #3815), as `points` lists them (#3833).
|
|
417
|
+
let heldOnLedger: Map<string, string> | undefined;
|
|
418
|
+
const { loaded, root, top, at, workspaceRoot, tree, result } = await readRecordsFor({
|
|
419
|
+
...query,
|
|
420
|
+
overlay: async (base, { loaded: l, root: r }) => {
|
|
421
|
+
if (!l.kind.answers) return base;
|
|
422
|
+
const { readLedgerAnswers, refreshLedger, withLedgerAnswers } = await import("./answers-ledger");
|
|
423
|
+
if (query.fetchLedger) await refreshLedger(l.file);
|
|
424
|
+
const ledger = await readLedgerAnswers({ file: l.file, name: l.kind.name, dirRel: relative(r, l.dir).split(sep).join("/") || "." });
|
|
425
|
+
heldOnLedger = new Map([...ledger.byPath.values()].map((a) => [a.path, a.ledger]));
|
|
426
|
+
return withLedgerAnswers(base, ledger);
|
|
427
|
+
},
|
|
428
|
+
});
|
|
402
429
|
// Provenance, judged by the policy at base and never by the tree read (#2547).
|
|
403
430
|
const base = top ? resolveBase(top, query.base) : { commit: null, from: null };
|
|
404
431
|
const policy = top ? policyAtBase(top, base) : policyAtBase(root, base);
|
|
@@ -426,6 +453,7 @@ export async function queryRecords(query: RecordsQuery): Promise<RecordsDocument
|
|
|
426
453
|
...(seals ? authorSeal(loaded.kind, r, recordPolicy, seals.checkRecordSeal) : {}),
|
|
427
454
|
});
|
|
428
455
|
}
|
|
456
|
+
if (heldOnLedger) for (const r of records) r.ledger = heldOnLedger.get(r.path) ?? null;
|
|
429
457
|
if (loaded.kind.approval && !loaded.kind.work && top) {
|
|
430
458
|
const decided = decidedCommits(top, at ?? "HEAD", records.map((r) => r.path), loaded.kind);
|
|
431
459
|
for (const r of records) r.decidedIn = decided.get(r.path) ?? null;
|
|
@@ -461,13 +489,15 @@ export async function queryRecords(query: RecordsQuery): Promise<RecordsDocument
|
|
|
461
489
|
if (at === null && top) {
|
|
462
490
|
const { head, ...rest } = readCheckoutHead(top, query.base);
|
|
463
491
|
const dir = relative(root, loaded.dir).split(sep).join("/") || ".";
|
|
492
|
+
// A record held on the ledger is not the checkout's: it has no worktree state, and is never uncommitted (#3833).
|
|
493
|
+
const inTree = records.filter((r) => !r.ledger);
|
|
464
494
|
const { states, deleted } = worktreeStates(top, head, dir, records.map((r) => r.path), new RegExp(loaded.kind.location.match));
|
|
465
|
-
for (const r of
|
|
495
|
+
for (const r of inTree) r.worktree = states.get(r.path) ?? "new";
|
|
466
496
|
const lastWrite = lastWriteReader(top);
|
|
467
|
-
for (const r of
|
|
497
|
+
for (const r of inTree) r.lastWrite = lastWrite(r.path, readTextOrNull(join(top, ...r.path.split("/"))));
|
|
468
498
|
checkout = { branch: rest.branch, head, base: rest.base, baseFrom: rest.baseFrom, deleted };
|
|
469
499
|
if (query.uncommitted) {
|
|
470
|
-
listed =
|
|
500
|
+
listed = inTree.filter((r) => r.worktree !== "committed");
|
|
471
501
|
summary = { ...summary, total: listed.length, valid: listed.filter((r) => r.valid).length, invalid: listed.filter((r) => !r.valid).length };
|
|
472
502
|
}
|
|
473
503
|
} else if (query.uncommitted && !top) {
|
|
@@ -664,7 +694,7 @@ export async function runWorkspaceRecords(ctx: CommandContext): Promise<number>
|
|
|
664
694
|
else console.log(formatSince(since));
|
|
665
695
|
return "error" in since ? 1 : 0;
|
|
666
696
|
}
|
|
667
|
-
const doc = await queryRecords({ kind: args.kind, current: args.current, uncommitted: args.uncommitted, at: args.at, base: args.base, cwd: process.cwd() });
|
|
697
|
+
const doc = await queryRecords({ kind: args.kind, current: args.current, uncommitted: args.uncommitted, at: args.at, base: args.base, cwd: process.cwd(), fetchLedger: true });
|
|
668
698
|
if (args.json) {
|
|
669
699
|
console.log(JSON.stringify(doc, null, 2));
|
|
670
700
|
} else if ("error" in doc) {
|
|
@@ -728,7 +758,7 @@ async function runDeclaredRecords(args: CommandContext["args"]): Promise<number>
|
|
|
728
758
|
return 1;
|
|
729
759
|
}
|
|
730
760
|
if (args.since !== undefined) return runDeclaredSince(declared, args);
|
|
731
|
-
const set = await queryDeclaredRecords(declared, { current: args.current, uncommitted: args.uncommitted, at: args.at, base: args.base, cwd: process.cwd() });
|
|
761
|
+
const set = await queryDeclaredRecords(declared, { current: args.current, uncommitted: args.uncommitted, at: args.at, base: args.base, cwd: process.cwd(), fetchLedger: true });
|
|
732
762
|
if (args.json) console.log(JSON.stringify(set, null, 2));
|
|
733
763
|
for (const doc of args.json ? [] : set.kinds) {
|
|
734
764
|
if ("error" in doc) {
|
|
@@ -827,7 +857,8 @@ function formatRecords(
|
|
|
827
857
|
const attested = r.provenance.level === "attested" ? ` attested by ${r.provenance.principal}` : "";
|
|
828
858
|
const sealed = r.attested === true ? ` sealed by ${(r.data?.[RECORD_SEAL_FIELD] as { signer: string }).signer}` : "";
|
|
829
859
|
const uncommitted = r.worktree === "new" || r.worktree === "modified" ? ` (${r.worktree}, uncommitted)` : "";
|
|
830
|
-
|
|
860
|
+
const ledger = r.ledger ? ` (held on ${r.ledger})` : "";
|
|
861
|
+
lines.push(`${(r.id ?? "-").padEnd(idWidth)} ${(r.state ?? "-").padEnd(stateWidth)} ${title}${superseded}${remediated}${attested}${sealed}${uncommitted}${ledger}${flag}`);
|
|
831
862
|
if (r.ready !== undefined) {
|
|
832
863
|
const blocked = (r.blockedBy ?? []).map((b) => `${b.id} (${b.state ?? "unknown"})`).join(", ");
|
|
833
864
|
const implemented = (r.implements ?? []).map((d) => `${d.id} (${d.state ?? "unknown"})`).join(", ");
|
|
@@ -327,6 +327,10 @@
|
|
|
327
327
|
"type": "array",
|
|
328
328
|
"items": { "$ref": "#/$defs/workAnswer" }
|
|
329
329
|
},
|
|
330
|
+
"ledger": {
|
|
331
|
+
"type": ["string", "null"],
|
|
332
|
+
"description": "Added in contract 1 by #3833, on every record of an answer kind (one with an answers block) read in the working tree; absent under --at. Where the record is held on the lifecycle ledger, chant/lifecycle:<path on the branch>, as chant workspace points names it for a question, or null when it is the working tree's file. An answer held on the ledger (a question a steward asked, or one asked with --store ledger) reads as if it sat in the kind's directory and wins over a file of the same name; it has no worktree or lastWrite and is never listed by --uncommitted. The command fetches chant/lifecycle first, best effort."
|
|
333
|
+
},
|
|
330
334
|
"lease": {
|
|
331
335
|
"description": "Added in contract 1 by #2732, on every record of a work kind with an id, read in the working tree (absent under --at). The item's active work lease (chant workspace work claim), or null when nobody holds one live. Read from the local lease refs, refs/chant/lease/[_members/<member>/]work/<id> and the remote's as last fetched, of the ledger owning the work kind file; records never fetches.",
|
|
332
336
|
"oneOf": [{ "type": "null" }, { "$ref": "#/$defs/workLease" }]
|