halfcycle 0.4.7 → 0.4.9
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/.claude-plugin/plugin.json +1 -1
- package/bin/bin.bundle.mjs +2217 -280
- package/commands/agent-teams-orchestration.md +1 -1
- package/commands/consistency-check.md +1 -1
- package/commands/context-update.md +1 -1
- package/commands/halfcycle-design.md +1 -1
- package/commands/halfcycle-setup.md +1 -1
- package/commands/stack-assembly-worker.md +1 -1
- package/commands/task-execution.md +1 -1
- package/commands/task-graph.md +1 -1
- package/commands/write-spec.md +1 -1
- package/dist/bin.d.ts +2 -1
- package/dist/bin.d.ts.map +1 -1
- package/dist/build-record/obtain.d.ts +14 -7
- package/dist/build-record/obtain.d.ts.map +1 -1
- package/dist/build-record/render-v2.d.ts +32 -6
- package/dist/build-record/render-v2.d.ts.map +1 -1
- package/dist/build-record/write.d.ts +2 -2
- package/dist/build-record/write.d.ts.map +1 -1
- package/dist/cli-contract.d.ts +2 -9
- package/dist/cli-contract.d.ts.map +1 -1
- package/dist/cli.js +3194 -1402
- package/dist/cli.js.map +3 -3
- package/dist/close-phase.d.ts +215 -134
- package/dist/close-phase.d.ts.map +1 -1
- package/dist/draw/index.d.ts +25 -5
- package/dist/draw/index.d.ts.map +1 -1
- package/dist/identity.d.ts +78 -9
- package/dist/identity.d.ts.map +1 -1
- package/dist/index.js +1629 -209
- package/dist/index.js.map +3 -3
- package/dist/install.d.ts +16 -0
- package/dist/install.d.ts.map +1 -1
- package/dist/loopback-signin.d.ts.map +1 -1
- package/dist/open-phase.d.ts +25 -1
- package/dist/open-phase.d.ts.map +1 -1
- package/dist/plan.d.ts +59 -18
- package/dist/plan.d.ts.map +1 -1
- package/dist/pre-create-checks.d.ts.map +1 -1
- package/dist/review.d.ts +228 -0
- package/dist/review.d.ts.map +1 -0
- package/dist/shown-record.d.ts +46 -0
- package/dist/shown-record.d.ts.map +1 -0
- package/dist/system-report.d.ts +28 -4
- package/dist/system-report.d.ts.map +1 -1
- package/package.json +1 -1
- package/dist/project-page.d.ts +0 -24
- package/dist/project-page.d.ts.map +0 -1
package/bin/bin.bundle.mjs
CHANGED
|
@@ -14781,33 +14781,69 @@ var HALFCYCLE_COMMAND = "npx halfcycle@latest";
|
|
|
14781
14781
|
|
|
14782
14782
|
// ../core/dist/shown-plans.js
|
|
14783
14783
|
var SHOWN_PLANS_FILENAME = "shown-plans.json";
|
|
14784
|
-
var
|
|
14785
|
-
var
|
|
14784
|
+
var SHOWN_RECORD_FORMAT = 2;
|
|
14785
|
+
var SHOWN_RECORD_FORMAT_1 = 1;
|
|
14786
|
+
var FORMAT_1_KEYS = ["entries", "format"];
|
|
14787
|
+
var RECORD_KEYS = ["entries", "format", "reviews"];
|
|
14788
|
+
var WRITTEN_KEYS = ["entries", "reviews"];
|
|
14786
14789
|
var ENTRY_KEYS = ["number", "phase", "shownAt", "version"];
|
|
14787
|
-
|
|
14790
|
+
var REVIEW_KEYS = ["number", "parts", "phase", "reviewVersion", "round", "shownAt"];
|
|
14791
|
+
var REVIEW_PART_KEYS = ["awaitingAnswer", "built", "decidable", "name", "part", "sentBack"];
|
|
14792
|
+
function nothingShown() {
|
|
14793
|
+
return { entries: [], reviews: [] };
|
|
14794
|
+
}
|
|
14795
|
+
function parseShownRecord(text) {
|
|
14788
14796
|
try {
|
|
14789
14797
|
if (typeof text !== "string")
|
|
14790
|
-
return
|
|
14798
|
+
return nothingShown();
|
|
14791
14799
|
const parsed = JSON.parse(text);
|
|
14792
|
-
if (!isPlainObject2(parsed)
|
|
14793
|
-
return
|
|
14794
|
-
|
|
14795
|
-
|
|
14796
|
-
|
|
14797
|
-
if (
|
|
14798
|
-
|
|
14799
|
-
|
|
14800
|
-
|
|
14801
|
-
|
|
14802
|
-
|
|
14803
|
-
|
|
14804
|
-
entries.push(entry);
|
|
14805
|
-
}
|
|
14806
|
-
return hasDuplicates(entries) ? [] : entries;
|
|
14800
|
+
if (!isPlainObject2(parsed))
|
|
14801
|
+
return nothingShown();
|
|
14802
|
+
let rawReviews;
|
|
14803
|
+
if (parsed["format"] === SHOWN_RECORD_FORMAT && hasExactKeys(parsed, RECORD_KEYS)) {
|
|
14804
|
+
rawReviews = parsed["reviews"];
|
|
14805
|
+
} else if (parsed["format"] === SHOWN_RECORD_FORMAT_1 && hasExactKeys(parsed, FORMAT_1_KEYS)) {
|
|
14806
|
+
rawReviews = [];
|
|
14807
|
+
} else {
|
|
14808
|
+
return nothingShown();
|
|
14809
|
+
}
|
|
14810
|
+
const record2 = readRecord(parsed["entries"], rawReviews);
|
|
14811
|
+
return record2 ?? nothingShown();
|
|
14807
14812
|
} catch {
|
|
14808
|
-
return
|
|
14813
|
+
return nothingShown();
|
|
14809
14814
|
}
|
|
14810
14815
|
}
|
|
14816
|
+
function serialiseShownRecord(record2) {
|
|
14817
|
+
if (!isPlainObject2(record2) || !hasExactKeys(record2, WRITTEN_KEYS))
|
|
14818
|
+
throw new Error(UNRECORDABLE);
|
|
14819
|
+
const copied = readRecord(record2["entries"], record2["reviews"]);
|
|
14820
|
+
if (copied === null)
|
|
14821
|
+
throw new Error(UNRECORDABLE);
|
|
14822
|
+
return `${JSON.stringify({ format: SHOWN_RECORD_FORMAT, entries: copied.entries, reviews: copied.reviews }, null, 2)}
|
|
14823
|
+
`;
|
|
14824
|
+
}
|
|
14825
|
+
var UNRECORDABLE = "the list just shown could not be recorded on this machine.";
|
|
14826
|
+
function readRecord(rawEntries, rawReviews) {
|
|
14827
|
+
if (!Array.isArray(rawEntries) || !Array.isArray(rawReviews))
|
|
14828
|
+
return null;
|
|
14829
|
+
const entries = [];
|
|
14830
|
+
for (const raw of rawEntries) {
|
|
14831
|
+
const entry = readEntry(raw);
|
|
14832
|
+
if (entry === null)
|
|
14833
|
+
return null;
|
|
14834
|
+
entries.push(entry);
|
|
14835
|
+
}
|
|
14836
|
+
const reviews = [];
|
|
14837
|
+
for (const raw of rawReviews) {
|
|
14838
|
+
const review = readReview(raw);
|
|
14839
|
+
if (review === null)
|
|
14840
|
+
return null;
|
|
14841
|
+
reviews.push(review);
|
|
14842
|
+
}
|
|
14843
|
+
if (hasDuplicates(entries) || hasDuplicates(reviews))
|
|
14844
|
+
return null;
|
|
14845
|
+
return { entries, reviews };
|
|
14846
|
+
}
|
|
14811
14847
|
function readEntry(raw) {
|
|
14812
14848
|
if (!isPlainObject2(raw) || !hasExactKeys(raw, ENTRY_KEYS))
|
|
14813
14849
|
return null;
|
|
@@ -14815,7 +14851,7 @@ function readEntry(raw) {
|
|
|
14815
14851
|
const number4 = raw["number"];
|
|
14816
14852
|
const version2 = raw["version"];
|
|
14817
14853
|
const shownAt = raw["shownAt"];
|
|
14818
|
-
if (
|
|
14854
|
+
if (!isNonEmptyString(phase))
|
|
14819
14855
|
return null;
|
|
14820
14856
|
if (!isPositiveInteger(number4) || !isPositiveInteger(version2))
|
|
14821
14857
|
return null;
|
|
@@ -14823,6 +14859,53 @@ function readEntry(raw) {
|
|
|
14823
14859
|
return null;
|
|
14824
14860
|
return { phase, number: number4, version: version2, shownAt };
|
|
14825
14861
|
}
|
|
14862
|
+
function readReview(raw) {
|
|
14863
|
+
if (!isPlainObject2(raw) || !hasExactKeys(raw, REVIEW_KEYS))
|
|
14864
|
+
return null;
|
|
14865
|
+
const phase = raw["phase"];
|
|
14866
|
+
const number4 = raw["number"];
|
|
14867
|
+
const round = raw["round"];
|
|
14868
|
+
const reviewVersion = raw["reviewVersion"];
|
|
14869
|
+
const shownAt = raw["shownAt"];
|
|
14870
|
+
const rawParts = raw["parts"];
|
|
14871
|
+
if (!isNonEmptyString(phase))
|
|
14872
|
+
return null;
|
|
14873
|
+
if (number4 !== null && !isPositiveInteger(number4))
|
|
14874
|
+
return null;
|
|
14875
|
+
if (!isPositiveInteger(round) || !isWholeNumber(reviewVersion))
|
|
14876
|
+
return null;
|
|
14877
|
+
if (!isIsoInstant(shownAt))
|
|
14878
|
+
return null;
|
|
14879
|
+
if (!Array.isArray(rawParts))
|
|
14880
|
+
return null;
|
|
14881
|
+
const parts = [];
|
|
14882
|
+
const seen = /* @__PURE__ */ new Set();
|
|
14883
|
+
for (const rawPart of rawParts) {
|
|
14884
|
+
const part = readReviewPart(rawPart);
|
|
14885
|
+
if (part === null || seen.has(part.part))
|
|
14886
|
+
return null;
|
|
14887
|
+
seen.add(part.part);
|
|
14888
|
+
parts.push(part);
|
|
14889
|
+
}
|
|
14890
|
+
return { phase, number: number4, round, reviewVersion, shownAt, parts };
|
|
14891
|
+
}
|
|
14892
|
+
function readReviewPart(raw) {
|
|
14893
|
+
if (!isPlainObject2(raw) || !hasExactKeys(raw, REVIEW_PART_KEYS))
|
|
14894
|
+
return null;
|
|
14895
|
+
const part = raw["part"];
|
|
14896
|
+
const name = raw["name"];
|
|
14897
|
+
const built = raw["built"];
|
|
14898
|
+
const decidable = raw["decidable"];
|
|
14899
|
+
const awaitingAnswer = raw["awaitingAnswer"];
|
|
14900
|
+
const sentBack = raw["sentBack"];
|
|
14901
|
+
if (!isNonEmptyString(part) || !isNonEmptyString(name))
|
|
14902
|
+
return null;
|
|
14903
|
+
if (typeof built !== "boolean" || typeof decidable !== "boolean")
|
|
14904
|
+
return null;
|
|
14905
|
+
if (typeof awaitingAnswer !== "boolean" || typeof sentBack !== "boolean")
|
|
14906
|
+
return null;
|
|
14907
|
+
return { part, name, built, decidable, awaitingAnswer, sentBack };
|
|
14908
|
+
}
|
|
14826
14909
|
function isPlainObject2(value) {
|
|
14827
14910
|
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
14828
14911
|
return false;
|
|
@@ -14833,19 +14916,26 @@ function hasExactKeys(value, keys) {
|
|
|
14833
14916
|
const own = Object.keys(value).sort();
|
|
14834
14917
|
return own.length === keys.length && own.every((key, i) => key === keys[i]);
|
|
14835
14918
|
}
|
|
14919
|
+
function isNonEmptyString(value) {
|
|
14920
|
+
return typeof value === "string" && value !== "";
|
|
14921
|
+
}
|
|
14836
14922
|
function isPositiveInteger(value) {
|
|
14837
14923
|
return typeof value === "number" && Number.isSafeInteger(value) && value >= 1;
|
|
14838
14924
|
}
|
|
14925
|
+
function isWholeNumber(value) {
|
|
14926
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
14927
|
+
}
|
|
14839
14928
|
function isIsoInstant(value) {
|
|
14840
14929
|
if (typeof value !== "string")
|
|
14841
14930
|
return false;
|
|
14842
14931
|
const ms = Date.parse(value);
|
|
14843
14932
|
return Number.isFinite(ms) && new Date(ms).toISOString() === value;
|
|
14844
14933
|
}
|
|
14845
|
-
function hasDuplicates(
|
|
14846
|
-
const phases = new Set(
|
|
14847
|
-
const
|
|
14848
|
-
|
|
14934
|
+
function hasDuplicates(items) {
|
|
14935
|
+
const phases = new Set(items.map((item) => item.phase));
|
|
14936
|
+
const numbered = items.filter((item) => item.number !== null);
|
|
14937
|
+
const numbers = new Set(numbered.map((item) => item.number));
|
|
14938
|
+
return phases.size !== items.length || numbers.size !== numbered.length;
|
|
14849
14939
|
}
|
|
14850
14940
|
|
|
14851
14941
|
// ../core/dist/phase-reference.js
|
|
@@ -14861,10 +14951,54 @@ function resolvePhaseReference(typed, phases) {
|
|
|
14861
14951
|
const byNumber = phases.filter((entry) => Number.isInteger(entry.number) && String(entry.number) === typed);
|
|
14862
14952
|
return byNumber.length === 1 ? byNumber[0] : null;
|
|
14863
14953
|
}
|
|
14954
|
+
function typedPhaseReference(phase, number4) {
|
|
14955
|
+
return phase.includes(":") && number4 !== null ? String(number4) : phase;
|
|
14956
|
+
}
|
|
14864
14957
|
|
|
14865
14958
|
// ../core/dist/release-version.js
|
|
14866
14959
|
var RELEASE_VERSION_PATTERN = /^\d+\.\d+\.\d+$/;
|
|
14867
14960
|
|
|
14961
|
+
// ../core/dist/github-address.js
|
|
14962
|
+
var GITHUB_REPOSITORY_ADDRESS = /^https:\/\/github\.com\/[A-Za-z0-9][A-Za-z0-9-]{0,38}\/(?!\.{1,2}$)[A-Za-z0-9._-]{1,100}$/;
|
|
14963
|
+
var HOST = "[Gg][Ii][Tt][Hh][Uu][Bb]\\.[Cc][Oo][Mm]";
|
|
14964
|
+
var OWNER = "[A-Za-z0-9][A-Za-z0-9-]{0,38}";
|
|
14965
|
+
var RAW_REPOSITORY = "[A-Za-z0-9._-]{1,104}";
|
|
14966
|
+
var TAIL = `(${OWNER})/(${RAW_REPOSITORY})/?$`;
|
|
14967
|
+
var URL_USERINFO = "(?:[^\\s/?#@]+@)?";
|
|
14968
|
+
var SSH_USER = "(?:[^\\s/?#@:]+@)?";
|
|
14969
|
+
var REMOTE_FORMS = [
|
|
14970
|
+
new RegExp(`^https?://${URL_USERINFO}${HOST}/${TAIL}`),
|
|
14971
|
+
new RegExp(`^ssh://${SSH_USER}${HOST}/${TAIL}`),
|
|
14972
|
+
new RegExp(`^git://${HOST}/${TAIL}`),
|
|
14973
|
+
new RegExp(`^${SSH_USER}${HOST}:${TAIL}`)
|
|
14974
|
+
];
|
|
14975
|
+
var REPOSITORY_NAME = /^(?!\.{1,2}$)[A-Za-z0-9._-]{1,100}$/;
|
|
14976
|
+
function githubRepositoryAddress(remote) {
|
|
14977
|
+
for (const form of REMOTE_FORMS) {
|
|
14978
|
+
const match = form.exec(remote);
|
|
14979
|
+
if (match === null)
|
|
14980
|
+
continue;
|
|
14981
|
+
const owner = match[1];
|
|
14982
|
+
const raw = match[2];
|
|
14983
|
+
if (owner === void 0 || raw === void 0)
|
|
14984
|
+
return null;
|
|
14985
|
+
const repository = raw.endsWith(".git") ? raw.slice(0, -4) : raw;
|
|
14986
|
+
if (!REPOSITORY_NAME.test(repository))
|
|
14987
|
+
return null;
|
|
14988
|
+
const address = `https://github.com/${owner}/${repository}`;
|
|
14989
|
+
if (!GITHUB_REPOSITORY_ADDRESS.test(address))
|
|
14990
|
+
return null;
|
|
14991
|
+
if (looksLikeSecret(address))
|
|
14992
|
+
return null;
|
|
14993
|
+
return address;
|
|
14994
|
+
}
|
|
14995
|
+
return null;
|
|
14996
|
+
}
|
|
14997
|
+
|
|
14998
|
+
// ../core/dist/sign-in.js
|
|
14999
|
+
var SIGN_IN_COMMAND = '"npx halfcycle"';
|
|
15000
|
+
var HOOK_REMEDY = `Run ${SIGN_IN_COMMAND} in the project folder to sign in.`;
|
|
15001
|
+
|
|
14868
15002
|
// dist/control-origin.js
|
|
14869
15003
|
var DEFAULT_CONTROL_ORIGIN = "https://control.halfcycle.ai";
|
|
14870
15004
|
function resolveControlOrigin(env) {
|
|
@@ -14899,8 +15033,8 @@ function readConfig(env = process.env) {
|
|
|
14899
15033
|
}
|
|
14900
15034
|
};
|
|
14901
15035
|
}
|
|
14902
|
-
function readNeverConfiguredReason() {
|
|
14903
|
-
return
|
|
15036
|
+
function readNeverConfiguredReason(env = process.env) {
|
|
15037
|
+
return env["HALFCYCLE_NOT_THIS_ACCOUNT"] === "1" ? "not-this-account" : "no-credential-yet";
|
|
14904
15038
|
}
|
|
14905
15039
|
function isHalfcycleOwnCi() {
|
|
14906
15040
|
return process.env["HALFCYCLE_INTERNAL_CI"] === "1";
|
|
@@ -15150,6 +15284,11 @@ function hasLoneSurrogate(text) {
|
|
|
15150
15284
|
}
|
|
15151
15285
|
var LONE_SURROGATE_MESSAGE = "must be valid text: it holds half of a character pair with no other half";
|
|
15152
15286
|
|
|
15287
|
+
// ../events/dist/human-agent.js
|
|
15288
|
+
var HUMAN_AGENT = "Human";
|
|
15289
|
+
var AGENT_NAME_MAX = 32;
|
|
15290
|
+
var agentNameSchema = external_exports.string().min(1).max(AGENT_NAME_MAX);
|
|
15291
|
+
|
|
15153
15292
|
// ../events/dist/state.js
|
|
15154
15293
|
var utcIso8601 = external_exports.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/, "must be a UTC ISO-8601 timestamp ending in Z");
|
|
15155
15294
|
var STATE_RECORD_FORMAT = "halfcycle-state-record/v1";
|
|
@@ -15273,6 +15412,7 @@ var coverageSchema = external_exports.object({
|
|
|
15273
15412
|
});
|
|
15274
15413
|
}
|
|
15275
15414
|
});
|
|
15415
|
+
var projectTierSchema = external_exports.number().int().min(0).max(3);
|
|
15276
15416
|
var artefactRecordSchema = stateRecordEnvelopeSchema.extend({
|
|
15277
15417
|
recordKind: external_exports.literal("artefact"),
|
|
15278
15418
|
artefactRef: artefactRefSchema,
|
|
@@ -15285,7 +15425,13 @@ var stepRunRecordSchema = stateRecordEnvelopeSchema.extend({
|
|
|
15285
15425
|
recordKind: external_exports.literal("step-run"),
|
|
15286
15426
|
runOutcome: runOutcomeSchema,
|
|
15287
15427
|
failureReason: external_exports.string().optional(),
|
|
15288
|
-
outcomeReason: external_exports.string().max(2e3).optional()
|
|
15428
|
+
outcomeReason: external_exports.string().max(2e3).optional(),
|
|
15429
|
+
// Who did this step, as the session reported it. Absent when not reported.
|
|
15430
|
+
agent: agentNameSchema.optional(),
|
|
15431
|
+
// When the session began this step. Absent when not reported.
|
|
15432
|
+
startedAt: utcIso8601.optional(),
|
|
15433
|
+
// The project's tier, when this step settled it. Absent on every other step.
|
|
15434
|
+
projectTier: projectTierSchema.optional()
|
|
15289
15435
|
}).strict().superRefine((rec, ctx) => {
|
|
15290
15436
|
if (rec.runOutcome === "attempted-failed" && rec.failureReason === void 0) {
|
|
15291
15437
|
ctx.addIssue({
|
|
@@ -15312,7 +15458,12 @@ var gateRecordSchema = stateRecordEnvelopeSchema.extend({
|
|
|
15312
15458
|
verdict: gateVerdictSchema,
|
|
15313
15459
|
actor: external_exports.string(),
|
|
15314
15460
|
outcomeReason: external_exports.string().max(2e3).optional(),
|
|
15315
|
-
coverage: coverageSchema.optional()
|
|
15461
|
+
coverage: coverageSchema.optional(),
|
|
15462
|
+
// Who did the step this checkpoint is on, as reported. Absent when not reported.
|
|
15463
|
+
agent: agentNameSchema.optional(),
|
|
15464
|
+
// When that step, or the review this checkpoint closes, began. Absent when not
|
|
15465
|
+
// reported.
|
|
15466
|
+
startedAt: utcIso8601.optional()
|
|
15316
15467
|
}).strict().superRefine((rec, ctx) => {
|
|
15317
15468
|
const issue2 = outcomeReasonIssue(rec.verdict, rec.outcomeReason);
|
|
15318
15469
|
if (issue2) {
|
|
@@ -15326,7 +15477,9 @@ var interventionRecordSchema = stateRecordEnvelopeSchema.extend({
|
|
|
15326
15477
|
severity: severitySchema,
|
|
15327
15478
|
disposition: dispositionSchema,
|
|
15328
15479
|
summary: external_exports.string().max(2e3),
|
|
15329
|
-
coverage: coverageSchema.optional()
|
|
15480
|
+
coverage: coverageSchema.optional(),
|
|
15481
|
+
// Who caught it, as the session reported. Absent when not reported.
|
|
15482
|
+
agent: agentNameSchema.optional()
|
|
15330
15483
|
}).strict();
|
|
15331
15484
|
var stateRecordSchema = external_exports.discriminatedUnion("recordKind", [
|
|
15332
15485
|
artefactRecordSchema,
|
|
@@ -15420,6 +15573,11 @@ var FINDINGS_SHOWN_MAX = 50;
|
|
|
15420
15573
|
var PHASE_CLOSE_REASON_MAX = 1900;
|
|
15421
15574
|
var closeCommitSchema = external_exports.string().regex(/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/, "must be a full commit hash: 40 or 64 lowercase hex characters");
|
|
15422
15575
|
var walkVerdictSchema = external_exports.enum(["clean", "defects"]);
|
|
15576
|
+
var acceptanceOutcomeSchema = external_exports.object({
|
|
15577
|
+
verdict: walkVerdictSchema,
|
|
15578
|
+
findings: external_exports.number().int().nonnegative(),
|
|
15579
|
+
findingsDetail: external_exports.array(external_exports.object({ summary: external_exports.string().refine((v) => !hasLoneSurrogate(v), { message: LONE_SURROGATE_MESSAGE }) }).strict())
|
|
15580
|
+
}).strict();
|
|
15423
15581
|
var closeDecisionSchema = external_exports.enum(["done-when-met", "override"]);
|
|
15424
15582
|
function plainText(max) {
|
|
15425
15583
|
const base = max === void 0 ? external_exports.string() : external_exports.string().max(max);
|
|
@@ -15543,12 +15701,7 @@ var attestedCloseSchema = external_exports.object({
|
|
|
15543
15701
|
var attestedSchema = external_exports.object({
|
|
15544
15702
|
closes: external_exports.array(attestedCloseSchema)
|
|
15545
15703
|
}).strict();
|
|
15546
|
-
|
|
15547
|
-
format: external_exports.literal(BUILD_RECORD_V2_FORMAT),
|
|
15548
|
-
phase: phaseNameSchema,
|
|
15549
|
-
observed: observedSchema,
|
|
15550
|
-
attested: attestedSchema
|
|
15551
|
-
}).strict().superRefine((record2, ctx) => {
|
|
15704
|
+
function checkRecordHalves(record2, ctx) {
|
|
15552
15705
|
const observedBySequence = new Map(record2.observed.closes.map((c) => [c.sequence, c]));
|
|
15553
15706
|
const attestedSequences = record2.attested.closes.map((c) => c.sequence);
|
|
15554
15707
|
const sameSet = attestedSequences.length === observedBySequence.size && new Set(attestedSequences).size === attestedSequences.length && attestedSequences.every((s) => observedBySequence.has(s));
|
|
@@ -15576,139 +15729,13 @@ var buildRecordV2Schema = external_exports.object({
|
|
|
15576
15729
|
});
|
|
15577
15730
|
}
|
|
15578
15731
|
});
|
|
15579
|
-
}
|
|
15580
|
-
|
|
15581
|
-
|
|
15582
|
-
|
|
15583
|
-
|
|
15584
|
-
|
|
15585
|
-
|
|
15586
|
-
"pending",
|
|
15587
|
-
"in-flight",
|
|
15588
|
-
"completed",
|
|
15589
|
-
"failed",
|
|
15590
|
-
"blocked"
|
|
15591
|
-
]);
|
|
15592
|
-
var BUILD_TASK_TITLE_MAX = 240;
|
|
15593
|
-
var BUILD_SNAPSHOT_TASK_LIMIT = 500;
|
|
15594
|
-
var buildTaskSchema = external_exports.object({
|
|
15595
|
-
// The task's own id, as the coordinator's graph names it.
|
|
15596
|
-
id: external_exports.string().min(1).max(64),
|
|
15597
|
-
// What the task is, in the coordinator's words. A description of the work,
|
|
15598
|
-
// bounded and scanned — never an excerpt of anybody's code.
|
|
15599
|
-
title: external_exports.string().min(1).max(BUILD_TASK_TITLE_MAX),
|
|
15600
|
-
// What the task is doing right now.
|
|
15601
|
-
state: buildTaskStateSchema,
|
|
15602
|
-
// Who holds it — a bare callsign, or the value naming a person when the work is
|
|
15603
|
-
// a person's. Absent when nothing has been dispatched to it yet.
|
|
15604
|
-
agent: external_exports.string().min(1).max(32).optional(),
|
|
15605
|
-
// The kind of work the holder does. Present exactly when an agent holds it.
|
|
15606
|
-
discipline: external_exports.string().min(1).max(64).optional(),
|
|
15607
|
-
// How much thinking the holder brings. Present exactly when an agent holds it,
|
|
15608
|
-
// and there is no field naming the supplier behind it.
|
|
15609
|
-
tier: external_exports.string().min(1).max(32).optional(),
|
|
15610
|
-
// The tasks this one cannot start before. Always present; empty means it waits
|
|
15611
|
-
// on nothing, which is different from not having been computed.
|
|
15612
|
-
dependsOn: external_exports.array(external_exports.string().min(1).max(64)).max(BUILD_SNAPSHOT_TASK_LIMIT),
|
|
15613
|
-
// The one task this task checks, when it is a check on other work. Absent on a
|
|
15614
|
-
// task that is doing the work rather than checking it.
|
|
15615
|
-
reviews: external_exports.string().min(1).max(64).optional(),
|
|
15616
|
-
// Which wave of the phase this task belongs to — the wave that writes the
|
|
15617
|
-
// specifications, or the wave that builds them. Absent when the graph does not
|
|
15618
|
-
// record it, which is different from the task belonging to neither.
|
|
15619
|
-
layer: methodLayerSchema.optional(),
|
|
15620
|
-
// When it was picked up, and when it settled. A reader computes how long it
|
|
15621
|
-
// took; neither this row nor this payload carries a duration.
|
|
15622
|
-
startedAt: utcIso8601.optional(),
|
|
15623
|
-
settledAt: utcIso8601.optional()
|
|
15624
|
-
}).strict().superRefine((task, ctx) => {
|
|
15625
|
-
if (task.reviews !== void 0 && task.reviews === task.id) {
|
|
15626
|
-
ctx.addIssue({
|
|
15627
|
-
code: external_exports.ZodIssueCode.custom,
|
|
15628
|
-
path: ["reviews"],
|
|
15629
|
-
message: `'${task.id}' reviews itself \u2014 a review names the OTHER task it checks.`
|
|
15630
|
-
});
|
|
15631
|
-
}
|
|
15632
|
-
if (task.layer !== void 0 && !isPhaseScopedLayer(task.layer)) {
|
|
15633
|
-
ctx.addIssue({
|
|
15634
|
-
code: external_exports.ZodIssueCode.custom,
|
|
15635
|
-
path: ["layer"],
|
|
15636
|
-
message: `'${task.layer}' is not a wave a phase runs \u2014 expected one of ${PHASE_SCOPED_LAYERS.join(", ")}.`
|
|
15637
|
-
});
|
|
15638
|
-
}
|
|
15639
|
-
const profiled = task.agent !== void 0 && task.agent !== HUMAN_AGENT;
|
|
15640
|
-
for (const field of ["discipline", "tier"]) {
|
|
15641
|
-
if (profiled && task[field] === void 0) {
|
|
15642
|
-
ctx.addIssue({
|
|
15643
|
-
code: external_exports.ZodIssueCode.custom,
|
|
15644
|
-
path: [field],
|
|
15645
|
-
message: `'${field}' is required on a task an agent holds.`
|
|
15646
|
-
});
|
|
15647
|
-
}
|
|
15648
|
-
if (!profiled && task[field] !== void 0) {
|
|
15649
|
-
ctx.addIssue({
|
|
15650
|
-
code: external_exports.ZodIssueCode.custom,
|
|
15651
|
-
path: [field],
|
|
15652
|
-
message: task.agent === void 0 ? `'${field}' must be absent on a task nobody holds.` : `'${field}' must be absent on work a person holds.`
|
|
15653
|
-
});
|
|
15654
|
-
}
|
|
15655
|
-
}
|
|
15656
|
-
});
|
|
15657
|
-
var buildPhaseSchema = external_exports.string().min(1).max(128);
|
|
15658
|
-
var BUILD_PHASE_TITLE_MAX = 160;
|
|
15659
|
-
var buildPhaseTitleSchema = external_exports.string().min(1).max(BUILD_PHASE_TITLE_MAX);
|
|
15660
|
-
var buildTaskListSchema = external_exports.array(buildTaskSchema).max(BUILD_SNAPSHOT_TASK_LIMIT).superRefine((tasks, ctx) => {
|
|
15661
|
-
const seen = /* @__PURE__ */ new Set();
|
|
15662
|
-
for (const [index, task] of tasks.entries()) {
|
|
15663
|
-
if (seen.has(task.id)) {
|
|
15664
|
-
ctx.addIssue({
|
|
15665
|
-
code: external_exports.ZodIssueCode.custom,
|
|
15666
|
-
path: [index, "id"],
|
|
15667
|
-
message: `duplicate task id '${task.id}' \u2014 one graph names each task once.`
|
|
15668
|
-
});
|
|
15669
|
-
}
|
|
15670
|
-
seen.add(task.id);
|
|
15671
|
-
}
|
|
15672
|
-
});
|
|
15673
|
-
var buildSnapshotSchema = external_exports.object({
|
|
15674
|
-
// Which phase this graph belongs to.
|
|
15675
|
-
phase: buildPhaseSchema,
|
|
15676
|
-
// That phase's name in words, when the project gives it one.
|
|
15677
|
-
phaseTitle: buildPhaseTitleSchema.optional(),
|
|
15678
|
-
// Every task on it. Never a partial graph and never a change since last time.
|
|
15679
|
-
tasks: buildTaskListSchema,
|
|
15680
|
-
// When this graph was last reported.
|
|
15681
|
-
reportedAt: utcIso8601,
|
|
15682
|
-
// When work on this phase was first reported with no approval of its scope in force.
|
|
15683
|
-
builtWithoutApproval: utcIso8601.optional()
|
|
15684
|
-
}).strict();
|
|
15685
|
-
var buildPhaseEntrySchema = external_exports.object({
|
|
15686
|
-
// Which phase has a stored graph.
|
|
15687
|
-
phase: buildPhaseSchema,
|
|
15688
|
-
// That phase's name in words, when the project gave it one.
|
|
15689
|
-
phaseTitle: buildPhaseTitleSchema.optional(),
|
|
15690
|
-
// When that phase's graph was last reported.
|
|
15691
|
-
reportedAt: utcIso8601,
|
|
15692
|
-
// Present when work on this phase was reported with no approval of its scope in force.
|
|
15693
|
-
builtWithoutApproval: external_exports.literal(true).optional()
|
|
15694
|
-
}).strict();
|
|
15695
|
-
var buildSnapshotReportSchema = external_exports.object({
|
|
15696
|
-
// Which phase this graph belongs to.
|
|
15697
|
-
phase: buildPhaseSchema,
|
|
15698
|
-
// That phase's name in words, when the project gives it one.
|
|
15699
|
-
phaseTitle: buildPhaseTitleSchema.optional(),
|
|
15700
|
-
// The whole graph, every time. Reporting a change instead of the graph is the
|
|
15701
|
-
// one thing this route does not accept.
|
|
15702
|
-
tasks: buildTaskListSchema
|
|
15703
|
-
}).strict();
|
|
15704
|
-
var buildSnapshotReportResultSchema = external_exports.object({
|
|
15705
|
-
// What happened. One outcome today; a refusal travels as the shared wire error.
|
|
15706
|
-
outcome: external_exports.literal("recorded"),
|
|
15707
|
-
// Which phase the graph was recorded against.
|
|
15708
|
-
phase: buildPhaseSchema,
|
|
15709
|
-
// When it was recorded.
|
|
15710
|
-
reportedAt: utcIso8601
|
|
15711
|
-
}).strict();
|
|
15732
|
+
}
|
|
15733
|
+
var buildRecordV2Schema = external_exports.object({
|
|
15734
|
+
format: external_exports.literal(BUILD_RECORD_V2_FORMAT),
|
|
15735
|
+
phase: phaseNameSchema,
|
|
15736
|
+
observed: observedSchema,
|
|
15737
|
+
attested: attestedSchema
|
|
15738
|
+
}).strict().superRefine(checkRecordHalves);
|
|
15712
15739
|
|
|
15713
15740
|
// ../events/dist/system-description.js
|
|
15714
15741
|
var SYSTEM_DESCRIPTION_FORMAT = 1;
|
|
@@ -15782,6 +15809,72 @@ var systemDescriptionProblemSchema = external_exports.object({
|
|
|
15782
15809
|
path: external_exports.string(),
|
|
15783
15810
|
message: external_exports.string()
|
|
15784
15811
|
}).strict();
|
|
15812
|
+
function segmentMatches(pattern, segment) {
|
|
15813
|
+
const literals = pattern.split("*");
|
|
15814
|
+
if (literals.length === 1)
|
|
15815
|
+
return pattern === segment;
|
|
15816
|
+
const first = literals[0];
|
|
15817
|
+
const last = literals[literals.length - 1];
|
|
15818
|
+
if (!segment.startsWith(first))
|
|
15819
|
+
return false;
|
|
15820
|
+
let position = first.length;
|
|
15821
|
+
for (const literal2 of literals.slice(1, -1)) {
|
|
15822
|
+
const found = segment.indexOf(literal2, position + 1);
|
|
15823
|
+
if (found === -1)
|
|
15824
|
+
return false;
|
|
15825
|
+
position = found + literal2.length;
|
|
15826
|
+
}
|
|
15827
|
+
return segment.length - last.length >= position + 1 && segment.endsWith(last);
|
|
15828
|
+
}
|
|
15829
|
+
function patternOwns(pattern, path) {
|
|
15830
|
+
const want = pattern.split("/");
|
|
15831
|
+
const have = path.split("/");
|
|
15832
|
+
const width = have.length + 1;
|
|
15833
|
+
const seen = new Uint8Array((want.length + 1) * width);
|
|
15834
|
+
const from = (i, j) => {
|
|
15835
|
+
if (i === want.length)
|
|
15836
|
+
return true;
|
|
15837
|
+
const cell = i * width + j;
|
|
15838
|
+
if (seen[cell] !== 0)
|
|
15839
|
+
return seen[cell] === 2;
|
|
15840
|
+
const segment = want[i];
|
|
15841
|
+
const result = segment === "**" ? from(i + 1, j) || j < have.length && from(i, j + 1) : j < have.length && segmentMatches(segment, have[j]) && from(i + 1, j + 1);
|
|
15842
|
+
seen[cell] = result ? 2 : 1;
|
|
15843
|
+
return result;
|
|
15844
|
+
};
|
|
15845
|
+
return from(0, 0);
|
|
15846
|
+
}
|
|
15847
|
+
function specificity(pattern) {
|
|
15848
|
+
const segments = pattern.split("/");
|
|
15849
|
+
const firstWildcard = segments.findIndex((segment) => segment.includes("*"));
|
|
15850
|
+
return {
|
|
15851
|
+
leadingLiterals: firstWildcard === -1 ? segments.length : firstWildcard,
|
|
15852
|
+
wildcards: segments.filter((segment) => segment.includes("*")).length,
|
|
15853
|
+
literalCharacters: pattern.replace(/[*/]/g, "").length
|
|
15854
|
+
};
|
|
15855
|
+
}
|
|
15856
|
+
function outranks(a, b) {
|
|
15857
|
+
if (a.rank.leadingLiterals !== b.rank.leadingLiterals)
|
|
15858
|
+
return b.rank.leadingLiterals - a.rank.leadingLiterals;
|
|
15859
|
+
if (a.rank.wildcards !== b.rank.wildcards)
|
|
15860
|
+
return a.rank.wildcards - b.rank.wildcards;
|
|
15861
|
+
if (a.rank.literalCharacters !== b.rank.literalCharacters)
|
|
15862
|
+
return b.rank.literalCharacters - a.rank.literalCharacters;
|
|
15863
|
+
return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
|
|
15864
|
+
}
|
|
15865
|
+
function ownerOf(path, description) {
|
|
15866
|
+
let best = null;
|
|
15867
|
+
for (const part of description.parts) {
|
|
15868
|
+
for (const pattern of part.owns) {
|
|
15869
|
+
if (!patternOwns(pattern, path))
|
|
15870
|
+
continue;
|
|
15871
|
+
const candidate = { id: part.id, rank: specificity(pattern) };
|
|
15872
|
+
if (best === null || outranks(candidate, best) < 0)
|
|
15873
|
+
best = candidate;
|
|
15874
|
+
}
|
|
15875
|
+
}
|
|
15876
|
+
return best === null ? null : best.id;
|
|
15877
|
+
}
|
|
15785
15878
|
var systemReportRecordedSchema = external_exports.object({
|
|
15786
15879
|
outcome: external_exports.literal("recorded"),
|
|
15787
15880
|
version: external_exports.number().int().min(1),
|
|
@@ -15811,9 +15904,14 @@ var systemDrawPhaseRequestSchema = external_exports.object({
|
|
|
15811
15904
|
phase: phaseIdentitySchema,
|
|
15812
15905
|
planVersion: external_exports.number().int().min(1)
|
|
15813
15906
|
}).strict();
|
|
15907
|
+
var systemDrawClosedPhaseRequestSchema = external_exports.object({
|
|
15908
|
+
picture: external_exports.literal("closed-phase"),
|
|
15909
|
+
phase: phaseIdentitySchema
|
|
15910
|
+
}).strict();
|
|
15814
15911
|
var systemDrawRequestSchema = external_exports.discriminatedUnion("picture", [
|
|
15815
15912
|
systemDrawArchitectureRequestSchema,
|
|
15816
|
-
systemDrawPhaseRequestSchema
|
|
15913
|
+
systemDrawPhaseRequestSchema,
|
|
15914
|
+
systemDrawClosedPhaseRequestSchema
|
|
15817
15915
|
]);
|
|
15818
15916
|
function utf8ByteLength2(text) {
|
|
15819
15917
|
let bytes = 0;
|
|
@@ -15845,12 +15943,180 @@ var systemPictureSchema = external_exports.object({
|
|
|
15845
15943
|
svg: external_exports.string().min(1).refine((v) => utf8ByteLength2(v) <= SYSTEM_DRAW_SVG_MAX_BYTES, { message: "the picture is larger than 2 MB" }),
|
|
15846
15944
|
png: external_exports.string().min(1).refine((v) => v.length % 4 === 0 && BASE64.test(v), { message: "the image is not valid base64" }).refine((v) => base64DecodedLength(v) <= SYSTEM_DRAW_PNG_MAX_BYTES, { message: "the image is larger than 8 MB" })
|
|
15847
15945
|
}).strict();
|
|
15848
|
-
var systemDrawRefusedSchema = external_exports.object({
|
|
15849
|
-
outcome: external_exports.literal("refused"),
|
|
15850
|
-
reason: external_exports.string().min(1)
|
|
15946
|
+
var systemDrawRefusedSchema = external_exports.object({
|
|
15947
|
+
outcome: external_exports.literal("refused"),
|
|
15948
|
+
reason: external_exports.string().min(1)
|
|
15949
|
+
}).strict();
|
|
15950
|
+
var systemDrawUnavailableSchema = external_exports.object({
|
|
15951
|
+
outcome: external_exports.literal("unavailable")
|
|
15952
|
+
}).strict();
|
|
15953
|
+
var systemDrawNotClosedSchema = external_exports.object({
|
|
15954
|
+
outcome: external_exports.literal("not-closed")
|
|
15955
|
+
}).strict();
|
|
15956
|
+
|
|
15957
|
+
// ../events/dist/build-snapshot.js
|
|
15958
|
+
var buildTaskStateSchema = external_exports.enum([
|
|
15959
|
+
"pending",
|
|
15960
|
+
"in-flight",
|
|
15961
|
+
"completed",
|
|
15962
|
+
"failed",
|
|
15963
|
+
"blocked"
|
|
15964
|
+
]);
|
|
15965
|
+
var BUILD_TASK_TITLE_MAX = 240;
|
|
15966
|
+
var BUILD_SNAPSHOT_TASK_LIMIT = 500;
|
|
15967
|
+
var BUILD_TASK_WHY_OUTSIDE_PLAN_MAX = 160;
|
|
15968
|
+
var BUILD_TASK_BUDGET_MAX_MINUTES = 10080;
|
|
15969
|
+
var BUILD_TASK_NOTE_MAX = 240;
|
|
15970
|
+
var BUILD_TASK_CLEAR_COMMAND_MAX = 240;
|
|
15971
|
+
var buildTaskKindSchema = external_exports.enum(["build", "review", "spec", "walk"]);
|
|
15972
|
+
var buildTaskIdSchema = external_exports.string().min(1).max(64);
|
|
15973
|
+
var buildTaskSchema = external_exports.object({
|
|
15974
|
+
// The task's own id, as the coordinator's graph names it.
|
|
15975
|
+
id: buildTaskIdSchema,
|
|
15976
|
+
// What the task is, in the coordinator's words. A description of the work,
|
|
15977
|
+
// bounded and scanned — never an excerpt of anybody's code.
|
|
15978
|
+
title: external_exports.string().min(1).max(BUILD_TASK_TITLE_MAX),
|
|
15979
|
+
// What the task is doing right now.
|
|
15980
|
+
state: buildTaskStateSchema,
|
|
15981
|
+
// Who holds it — a bare callsign, or the value naming a person when the work is
|
|
15982
|
+
// a person's. Absent when nothing has been dispatched to it yet.
|
|
15983
|
+
agent: agentNameSchema.optional(),
|
|
15984
|
+
// The kind of work the holder does. Present exactly when an agent holds it.
|
|
15985
|
+
discipline: external_exports.string().min(1).max(64).optional(),
|
|
15986
|
+
// How much thinking the holder brings. Present exactly when an agent holds it,
|
|
15987
|
+
// and there is no field naming the supplier behind it.
|
|
15988
|
+
tier: external_exports.string().min(1).max(32).optional(),
|
|
15989
|
+
// The tasks this one cannot start before. Always present; empty means it waits
|
|
15990
|
+
// on nothing, which is different from not having been computed.
|
|
15991
|
+
dependsOn: external_exports.array(buildTaskIdSchema).max(BUILD_SNAPSHOT_TASK_LIMIT),
|
|
15992
|
+
// The one task this task checks, when it is a check on other work. Absent on a
|
|
15993
|
+
// task that is doing the work rather than checking it.
|
|
15994
|
+
reviews: buildTaskIdSchema.optional(),
|
|
15995
|
+
// Which wave of the phase this task belongs to — the wave that writes the
|
|
15996
|
+
// specifications, or the wave that builds them. Absent when the graph does not
|
|
15997
|
+
// record it, which is different from the task belonging to neither.
|
|
15998
|
+
layer: methodLayerSchema.optional(),
|
|
15999
|
+
// When it was picked up, and when it settled. A reader computes how long it
|
|
16000
|
+
// took; neither this row nor this payload carries a duration.
|
|
16001
|
+
startedAt: utcIso8601.optional(),
|
|
16002
|
+
settledAt: utcIso8601.optional(),
|
|
16003
|
+
// The parts of this project's system the task builds, by their ids. Absent when
|
|
16004
|
+
// it builds none; never an empty list.
|
|
16005
|
+
builds: external_exports.array(systemPartIdSchema).min(1).max(SYSTEM_PARTS_MAX).optional(),
|
|
16006
|
+
// In one line, why the task built a part the phase's plan does not include, in
|
|
16007
|
+
// the words of whoever holds it. Absent when it gave no reason.
|
|
16008
|
+
whyOutsidePlan: descriptionText(BUILD_TASK_WHY_OUTSIDE_PLAN_MAX).optional(),
|
|
16009
|
+
// How long the task is expected to take, in whole minutes, from one minute to one
|
|
16010
|
+
// week. A plan, not a measurement; absent when no budget was set.
|
|
16011
|
+
budgetMinutes: external_exports.number().int().min(1).max(BUILD_TASK_BUDGET_MAX_MINUTES).optional(),
|
|
16012
|
+
// What kind of work the task is: building, checking other work, writing what to
|
|
16013
|
+
// build, or a person trying the product. Absent when the graph does not say.
|
|
16014
|
+
kind: buildTaskKindSchema.optional(),
|
|
16015
|
+
// One line from whoever holds the task about where it stands. Absent when there
|
|
16016
|
+
// is nothing to say.
|
|
16017
|
+
note: descriptionText(BUILD_TASK_NOTE_MAX).optional(),
|
|
16018
|
+
// On a task held for a person, the one thing they do or run to clear it, in one
|
|
16019
|
+
// line. Absent when the task names none.
|
|
16020
|
+
clearCommand: descriptionText(BUILD_TASK_CLEAR_COMMAND_MAX).optional(),
|
|
16021
|
+
// On a task held for a person, the crew member ready to take it on once it is
|
|
16022
|
+
// cleared, by callsign. Absent when nobody is standing by.
|
|
16023
|
+
standingBy: agentNameSchema.optional()
|
|
16024
|
+
}).strict().superRefine((task, ctx) => {
|
|
16025
|
+
if (task.reviews !== void 0 && task.reviews === task.id) {
|
|
16026
|
+
ctx.addIssue({
|
|
16027
|
+
code: external_exports.ZodIssueCode.custom,
|
|
16028
|
+
path: ["reviews"],
|
|
16029
|
+
message: `'${task.id}' reviews itself \u2014 a review names the OTHER task it checks.`
|
|
16030
|
+
});
|
|
16031
|
+
}
|
|
16032
|
+
if (task.layer !== void 0 && !isPhaseScopedLayer(task.layer)) {
|
|
16033
|
+
ctx.addIssue({
|
|
16034
|
+
code: external_exports.ZodIssueCode.custom,
|
|
16035
|
+
path: ["layer"],
|
|
16036
|
+
message: `'${task.layer}' is not a wave a phase runs \u2014 expected one of ${PHASE_SCOPED_LAYERS.join(", ")}.`
|
|
16037
|
+
});
|
|
16038
|
+
}
|
|
16039
|
+
const profiled = task.agent !== void 0 && task.agent !== HUMAN_AGENT;
|
|
16040
|
+
for (const field of ["discipline", "tier"]) {
|
|
16041
|
+
if (profiled && task[field] === void 0) {
|
|
16042
|
+
ctx.addIssue({
|
|
16043
|
+
code: external_exports.ZodIssueCode.custom,
|
|
16044
|
+
path: [field],
|
|
16045
|
+
message: `'${field}' is required on a task an agent holds.`
|
|
16046
|
+
});
|
|
16047
|
+
}
|
|
16048
|
+
if (!profiled && task[field] !== void 0) {
|
|
16049
|
+
ctx.addIssue({
|
|
16050
|
+
code: external_exports.ZodIssueCode.custom,
|
|
16051
|
+
path: [field],
|
|
16052
|
+
message: task.agent === void 0 ? `'${field}' must be absent on a task nobody holds.` : `'${field}' must be absent on work a person holds.`
|
|
16053
|
+
});
|
|
16054
|
+
}
|
|
16055
|
+
}
|
|
16056
|
+
});
|
|
16057
|
+
var buildPhaseSchema = external_exports.string().min(1).max(128);
|
|
16058
|
+
var BUILD_PHASE_TITLE_MAX = 160;
|
|
16059
|
+
var buildPhaseTitleSchema = external_exports.string().min(1).max(BUILD_PHASE_TITLE_MAX);
|
|
16060
|
+
var buildTaskListSchema = external_exports.array(buildTaskSchema).max(BUILD_SNAPSHOT_TASK_LIMIT).superRefine((tasks, ctx) => {
|
|
16061
|
+
const seen = /* @__PURE__ */ new Set();
|
|
16062
|
+
for (const [index, task] of tasks.entries()) {
|
|
16063
|
+
if (seen.has(task.id)) {
|
|
16064
|
+
ctx.addIssue({
|
|
16065
|
+
code: external_exports.ZodIssueCode.custom,
|
|
16066
|
+
path: [index, "id"],
|
|
16067
|
+
message: `duplicate task id '${task.id}' \u2014 one graph names each task once.`
|
|
16068
|
+
});
|
|
16069
|
+
}
|
|
16070
|
+
seen.add(task.id);
|
|
16071
|
+
}
|
|
16072
|
+
});
|
|
16073
|
+
var buildTaskMarkSchema = external_exports.object({
|
|
16074
|
+
// The task that was marked.
|
|
16075
|
+
task: buildTaskIdSchema,
|
|
16076
|
+
// When it was first marked.
|
|
16077
|
+
at: utcIso8601
|
|
16078
|
+
}).strict();
|
|
16079
|
+
var buildSnapshotSchema = external_exports.object({
|
|
16080
|
+
// Which phase this graph belongs to.
|
|
16081
|
+
phase: buildPhaseSchema,
|
|
16082
|
+
// That phase's name in words, when the project gives it one.
|
|
16083
|
+
phaseTitle: buildPhaseTitleSchema.optional(),
|
|
16084
|
+
// Every task on it. Never a partial graph and never a change since last time.
|
|
16085
|
+
tasks: buildTaskListSchema,
|
|
16086
|
+
// When this graph was last reported.
|
|
16087
|
+
reportedAt: utcIso8601,
|
|
16088
|
+
// When work on this phase was first reported with no approval of its scope in force.
|
|
16089
|
+
builtWithoutApproval: utcIso8601.optional(),
|
|
16090
|
+
// Each task that started on a part still waiting for approval, and when it was
|
|
16091
|
+
// first seen doing so. Absent when there is none.
|
|
16092
|
+
tasksBuiltWithoutApproval: external_exports.array(buildTaskMarkSchema).min(1).max(BUILD_SNAPSHOT_TASK_LIMIT).optional()
|
|
15851
16093
|
}).strict();
|
|
15852
|
-
var
|
|
15853
|
-
|
|
16094
|
+
var buildPhaseEntrySchema = external_exports.object({
|
|
16095
|
+
// Which phase has a stored graph.
|
|
16096
|
+
phase: buildPhaseSchema,
|
|
16097
|
+
// That phase's name in words, when the project gave it one.
|
|
16098
|
+
phaseTitle: buildPhaseTitleSchema.optional(),
|
|
16099
|
+
// When that phase's graph was last reported.
|
|
16100
|
+
reportedAt: utcIso8601,
|
|
16101
|
+
// Present when work on this phase was reported with no approval of its scope in force.
|
|
16102
|
+
builtWithoutApproval: external_exports.literal(true).optional()
|
|
16103
|
+
}).strict();
|
|
16104
|
+
var buildSnapshotReportSchema = external_exports.object({
|
|
16105
|
+
// Which phase this graph belongs to.
|
|
16106
|
+
phase: buildPhaseSchema,
|
|
16107
|
+
// That phase's name in words, when the project gives it one.
|
|
16108
|
+
phaseTitle: buildPhaseTitleSchema.optional(),
|
|
16109
|
+
// The whole graph, every time. Reporting a change instead of the graph is the
|
|
16110
|
+
// one thing this route does not accept.
|
|
16111
|
+
tasks: buildTaskListSchema
|
|
16112
|
+
}).strict();
|
|
16113
|
+
var buildSnapshotReportResultSchema = external_exports.object({
|
|
16114
|
+
// What happened. One outcome today; a refusal travels as the shared wire error.
|
|
16115
|
+
outcome: external_exports.literal("recorded"),
|
|
16116
|
+
// Which phase the graph was recorded against.
|
|
16117
|
+
phase: buildPhaseSchema,
|
|
16118
|
+
// When it was recorded.
|
|
16119
|
+
reportedAt: utcIso8601
|
|
15854
16120
|
}).strict();
|
|
15855
16121
|
|
|
15856
16122
|
// ../events/dist/phase-plan.js
|
|
@@ -15904,6 +16170,13 @@ var scopeDecisionPartSchema = external_exports.object({
|
|
|
15904
16170
|
note: scopeNoteWireSchema.optional()
|
|
15905
16171
|
}).strict();
|
|
15906
16172
|
var planVersionSchema = external_exports.number().int().min(1);
|
|
16173
|
+
var carriedPhaseSchema = external_exports.object({
|
|
16174
|
+
phase: scopePhaseSchema,
|
|
16175
|
+
number: external_exports.number().int().min(1)
|
|
16176
|
+
}).strict();
|
|
16177
|
+
var carriedFromEntrySchema = carriedPhaseSchema.extend({
|
|
16178
|
+
part: systemPartIdSchema
|
|
16179
|
+
}).strict();
|
|
15907
16180
|
var phasePlanRequestSchema = external_exports.object({
|
|
15908
16181
|
descriptionVersion: external_exports.number().int().min(1),
|
|
15909
16182
|
plan: external_exports.unknown()
|
|
@@ -15915,7 +16188,8 @@ var phasePlanRecordedSchema = external_exports.object({
|
|
|
15915
16188
|
state: plannedScopeStateSchema,
|
|
15916
16189
|
changedParts: external_exports.array(systemPartIdSchema).max(PHASE_PLAN_CHANGES_MAX),
|
|
15917
16190
|
typedApprovalOffered: external_exports.boolean(),
|
|
15918
|
-
reportedAt: utcIso8601
|
|
16191
|
+
reportedAt: utcIso8601,
|
|
16192
|
+
carriedFrom: external_exports.array(carriedFromEntrySchema).min(1).max(PHASE_PLAN_CHANGES_MAX).optional()
|
|
15919
16193
|
}).strict();
|
|
15920
16194
|
var phasePlanRefusedSchema = external_exports.object({
|
|
15921
16195
|
outcome: external_exports.literal("refused"),
|
|
@@ -15924,10 +16198,23 @@ var phasePlanRefusedSchema = external_exports.object({
|
|
|
15924
16198
|
var phasePlanUnavailableSchema = external_exports.object({
|
|
15925
16199
|
outcome: external_exports.literal("unavailable")
|
|
15926
16200
|
}).strict();
|
|
16201
|
+
var REMOVAL_OWED_ACTS = ["dropped", "removed"];
|
|
16202
|
+
var removalOwedPartSchema = external_exports.object({
|
|
16203
|
+
part: systemPartIdSchema,
|
|
16204
|
+
name: systemPartSchema.shape.name,
|
|
16205
|
+
phase: scopePhaseSchema,
|
|
16206
|
+
number: external_exports.number().int().min(1),
|
|
16207
|
+
act: external_exports.enum(REMOVAL_OWED_ACTS)
|
|
16208
|
+
}).strict();
|
|
16209
|
+
var phasePlanRemovalOwedSchema = external_exports.object({
|
|
16210
|
+
outcome: external_exports.literal("removal-owed"),
|
|
16211
|
+
parts: external_exports.array(removalOwedPartSchema).min(1).max(SYSTEM_PARTS_MAX)
|
|
16212
|
+
}).strict();
|
|
15927
16213
|
var phasePlanResultSchema = external_exports.discriminatedUnion("outcome", [
|
|
15928
16214
|
phasePlanRecordedSchema,
|
|
15929
16215
|
phasePlanRefusedSchema,
|
|
15930
|
-
phasePlanUnavailableSchema
|
|
16216
|
+
phasePlanUnavailableSchema,
|
|
16217
|
+
phasePlanRemovalOwedSchema
|
|
15931
16218
|
]);
|
|
15932
16219
|
var scopeApproveRequestSchema = external_exports.object({
|
|
15933
16220
|
act: external_exports.literal("approve"),
|
|
@@ -15989,7 +16276,8 @@ var scopeReadPartSchema = external_exports.object({
|
|
|
15989
16276
|
change: phasePlanChangeSchema,
|
|
15990
16277
|
summary: phasePlanSummarySchema,
|
|
15991
16278
|
status: scopePartStatusSchema,
|
|
15992
|
-
changed: external_exports.boolean()
|
|
16279
|
+
changed: external_exports.boolean(),
|
|
16280
|
+
carriedFrom: carriedPhaseSchema.optional()
|
|
15993
16281
|
}).strict();
|
|
15994
16282
|
var scopeReadDecisionSchema = external_exports.object({
|
|
15995
16283
|
act: scopeActSchema,
|
|
@@ -16016,9 +16304,580 @@ var scopeReadPlannedSchema = external_exports.object({
|
|
|
16016
16304
|
}).strict();
|
|
16017
16305
|
var scopeReadSchema = external_exports.union([scopeReadNotPlannedSchema, scopeReadPlannedSchema]);
|
|
16018
16306
|
|
|
16307
|
+
// ../events/dist/code-link.js
|
|
16308
|
+
var codeLinkAddressSchema = external_exports.string().regex(GITHUB_REPOSITORY_ADDRESS, { message: "must be a GitHub repository address: https://github.com/<owner>/<repo>" }).refine((v) => !looksLikeSecret(v), { message: "looks like a password or key, so it is not sent" });
|
|
16309
|
+
var codeLinkFactsSchema = external_exports.object({
|
|
16310
|
+
repositoryAddress: codeLinkAddressSchema.nullable(),
|
|
16311
|
+
headOnGitHub: external_exports.boolean()
|
|
16312
|
+
}).strict().refine((v) => !(v.headOnGitHub && v.repositoryAddress === null), {
|
|
16313
|
+
message: "cannot be true when the code is not on GitHub",
|
|
16314
|
+
path: ["headOnGitHub"]
|
|
16315
|
+
});
|
|
16316
|
+
var PART_CODE_LINK_SCOPES = ["part", "phase"];
|
|
16317
|
+
var PART_CODE_LINK_RANGE_REASONS = ["not-github", "not-pushed"];
|
|
16318
|
+
var PART_CODE_LINK_URL_PREFIX = "https://github.com/";
|
|
16319
|
+
var partCodeLinkGithubSchema = external_exports.object({
|
|
16320
|
+
kind: external_exports.literal("github"),
|
|
16321
|
+
scope: external_exports.enum(PART_CODE_LINK_SCOPES),
|
|
16322
|
+
url: external_exports.string().startsWith(PART_CODE_LINK_URL_PREFIX, { message: "must start with https://github.com/" })
|
|
16323
|
+
}).strict();
|
|
16324
|
+
var partCodeLinkRangeSchema = external_exports.object({
|
|
16325
|
+
kind: external_exports.literal("range"),
|
|
16326
|
+
why: external_exports.enum(PART_CODE_LINK_RANGE_REASONS),
|
|
16327
|
+
from: closeCommitSchema,
|
|
16328
|
+
to: closeCommitSchema
|
|
16329
|
+
}).strict();
|
|
16330
|
+
var partCodeLinkNoneSchema = external_exports.object({
|
|
16331
|
+
kind: external_exports.literal("none")
|
|
16332
|
+
}).strict();
|
|
16333
|
+
var partCodeLinkSchema = external_exports.discriminatedUnion("kind", [
|
|
16334
|
+
partCodeLinkGithubSchema,
|
|
16335
|
+
partCodeLinkRangeSchema,
|
|
16336
|
+
partCodeLinkNoneSchema
|
|
16337
|
+
]);
|
|
16338
|
+
|
|
16339
|
+
// ../events/dist/observed-changes.js
|
|
16340
|
+
var OBSERVED_PATHS_LISTED_MAX = 50;
|
|
16341
|
+
var OBSERVED_PATH_MAX = 300;
|
|
16342
|
+
var OBSERVED_COUNT_MAX = 1e6;
|
|
16343
|
+
var METHOD_FILE_PATTERNS = [
|
|
16344
|
+
"CLAUDE.md",
|
|
16345
|
+
"AGENTS.md",
|
|
16346
|
+
".claude/context",
|
|
16347
|
+
".claude/commands",
|
|
16348
|
+
".claude/hooks",
|
|
16349
|
+
".claude/settings.json",
|
|
16350
|
+
".mcp.json",
|
|
16351
|
+
".halfcycle",
|
|
16352
|
+
".workbench",
|
|
16353
|
+
"docs/product-spec.md",
|
|
16354
|
+
"docs/architecture-overview.md",
|
|
16355
|
+
"docs/invariants.md",
|
|
16356
|
+
"docs/project-log.md",
|
|
16357
|
+
"docs/code-map.md",
|
|
16358
|
+
"docs/system.json",
|
|
16359
|
+
"docs/diagrams",
|
|
16360
|
+
"docs/features",
|
|
16361
|
+
"docs/phases",
|
|
16362
|
+
"docs/build-records",
|
|
16363
|
+
"docs/decisions",
|
|
16364
|
+
"docs/components"
|
|
16365
|
+
];
|
|
16366
|
+
var TEST_FILE_PATTERNS = [
|
|
16367
|
+
"**/*.test.*",
|
|
16368
|
+
"**/*.spec.*",
|
|
16369
|
+
"**/*_test.*",
|
|
16370
|
+
"**/*_spec.*",
|
|
16371
|
+
"**/test_*.*",
|
|
16372
|
+
"**/*Test.java",
|
|
16373
|
+
"**/*Tests.java",
|
|
16374
|
+
"**/*Test.kt",
|
|
16375
|
+
"**/*Tests.cs",
|
|
16376
|
+
"**/*Test.php"
|
|
16377
|
+
];
|
|
16378
|
+
function isMethodFile(path) {
|
|
16379
|
+
return METHOD_FILE_PATTERNS.some((pattern) => patternOwns(pattern, path));
|
|
16380
|
+
}
|
|
16381
|
+
function isTestFile(path) {
|
|
16382
|
+
return TEST_FILE_PATTERNS.some((pattern) => patternOwns(pattern, path));
|
|
16383
|
+
}
|
|
16384
|
+
var observedCountSchema = external_exports.number({ error: `must be a whole number from 0 to ${OBSERVED_COUNT_MAX}` }).int({ error: `must be a whole number from 0 to ${OBSERVED_COUNT_MAX}` }).min(0, { error: `must be a whole number from 0 to ${OBSERVED_COUNT_MAX}` }).max(OBSERVED_COUNT_MAX, { error: `must be a whole number from 0 to ${OBSERVED_COUNT_MAX}` });
|
|
16385
|
+
var observedDescriptionVersionSchema = external_exports.number().int().min(1);
|
|
16386
|
+
var observedPathSchema = external_exports.string().min(1, { message: "must not be empty" }).max(OBSERVED_PATH_MAX, { message: `must be at most ${OBSERVED_PATH_MAX} characters` });
|
|
16387
|
+
var observedPathListSchema = external_exports.array(observedPathSchema).max(OBSERVED_PATHS_LISTED_MAX, { message: `can list at most ${OBSERVED_PATHS_LISTED_MAX} paths` }).refine((paths) => new Set(paths).size === paths.length, { message: "must name each path once" });
|
|
16388
|
+
var observedMethodFilesSchema = external_exports.object({
|
|
16389
|
+
added: observedCountSchema,
|
|
16390
|
+
changed: observedCountSchema,
|
|
16391
|
+
removed: observedCountSchema
|
|
16392
|
+
}).strict();
|
|
16393
|
+
var observedFilesSchema = external_exports.object({
|
|
16394
|
+
added: observedCountSchema,
|
|
16395
|
+
changed: observedCountSchema,
|
|
16396
|
+
removed: observedCountSchema,
|
|
16397
|
+
testFilesAdded: observedCountSchema
|
|
16398
|
+
}).strict().refine((v) => v.testFilesAdded <= v.added, {
|
|
16399
|
+
message: "cannot be more than the files added",
|
|
16400
|
+
path: ["testFilesAdded"]
|
|
16401
|
+
});
|
|
16402
|
+
function filesChanged(files) {
|
|
16403
|
+
return files.added + files.changed + files.removed;
|
|
16404
|
+
}
|
|
16405
|
+
function pathsFitTheCount(files, paths) {
|
|
16406
|
+
return files === null ? paths.length === 0 : paths.length <= filesChanged(files);
|
|
16407
|
+
}
|
|
16408
|
+
function checkPartsUnique(parts, ctx, at) {
|
|
16409
|
+
const seen = /* @__PURE__ */ new Set();
|
|
16410
|
+
parts.forEach((entry, i) => {
|
|
16411
|
+
if (seen.has(entry.part)) {
|
|
16412
|
+
ctx.addIssue({ code: "custom", path: [...at, i, "part"], message: `names the part "${entry.part}" twice` });
|
|
16413
|
+
}
|
|
16414
|
+
seen.add(entry.part);
|
|
16415
|
+
});
|
|
16416
|
+
}
|
|
16417
|
+
var observedBasisCountSchema = external_exports.object({
|
|
16418
|
+
outcome: external_exports.literal("count"),
|
|
16419
|
+
phase: phaseNameSchema,
|
|
16420
|
+
startCommit: closeCommitSchema,
|
|
16421
|
+
descriptionVersion: observedDescriptionVersionSchema,
|
|
16422
|
+
description: systemDescriptionSchema
|
|
16423
|
+
}).strict();
|
|
16424
|
+
var observedBasisCannotCountSchema = external_exports.object({
|
|
16425
|
+
outcome: external_exports.literal("cannot-count"),
|
|
16426
|
+
phase: phaseNameSchema,
|
|
16427
|
+
reason: external_exports.literal("no-start-commit")
|
|
16428
|
+
}).strict();
|
|
16429
|
+
var observedBasisNothingToCountSchema = external_exports.object({
|
|
16430
|
+
outcome: external_exports.literal("nothing-to-count"),
|
|
16431
|
+
reason: external_exports.enum(["no-open-phase", "no-description"])
|
|
16432
|
+
}).strict();
|
|
16433
|
+
var observedBasisSchema = external_exports.discriminatedUnion("outcome", [
|
|
16434
|
+
observedBasisCountSchema,
|
|
16435
|
+
observedBasisCannotCountSchema,
|
|
16436
|
+
observedBasisNothingToCountSchema
|
|
16437
|
+
]);
|
|
16438
|
+
var observedReportPartSchema = external_exports.object({
|
|
16439
|
+
part: systemPartIdSchema,
|
|
16440
|
+
added: observedCountSchema,
|
|
16441
|
+
changed: observedCountSchema,
|
|
16442
|
+
removed: observedCountSchema,
|
|
16443
|
+
testFilesAdded: observedCountSchema
|
|
16444
|
+
}).strict().refine((v) => v.testFilesAdded <= v.added, {
|
|
16445
|
+
message: "cannot be more than the files added",
|
|
16446
|
+
path: ["testFilesAdded"]
|
|
16447
|
+
}).refine((v) => filesChanged(v) >= 1, {
|
|
16448
|
+
message: "a part is listed only when at least one file changed under it",
|
|
16449
|
+
path: ["added"]
|
|
16450
|
+
});
|
|
16451
|
+
var observedReportOutsideSchema = external_exports.object({
|
|
16452
|
+
added: observedCountSchema,
|
|
16453
|
+
changed: observedCountSchema,
|
|
16454
|
+
removed: observedCountSchema,
|
|
16455
|
+
testFilesAdded: observedCountSchema,
|
|
16456
|
+
paths: observedPathListSchema
|
|
16457
|
+
}).strict().refine((v) => v.testFilesAdded <= v.added, {
|
|
16458
|
+
message: "cannot be more than the files added",
|
|
16459
|
+
path: ["testFilesAdded"]
|
|
16460
|
+
}).refine((v) => pathsFitTheCount(v, v.paths), {
|
|
16461
|
+
message: "lists more paths than files changed",
|
|
16462
|
+
path: ["paths"]
|
|
16463
|
+
});
|
|
16464
|
+
var observedChangesReportSchema = external_exports.object({
|
|
16465
|
+
phase: phaseNameSchema,
|
|
16466
|
+
startCommit: closeCommitSchema,
|
|
16467
|
+
headCommit: closeCommitSchema,
|
|
16468
|
+
headCommittedAt: utcIso8601,
|
|
16469
|
+
commits: observedCountSchema,
|
|
16470
|
+
descriptionVersion: observedDescriptionVersionSchema,
|
|
16471
|
+
parts: external_exports.array(observedReportPartSchema).max(SYSTEM_PARTS_MAX, { message: `can list at most ${SYSTEM_PARTS_MAX} parts` }),
|
|
16472
|
+
methodFiles: observedMethodFilesSchema,
|
|
16473
|
+
outsideEveryPart: observedReportOutsideSchema,
|
|
16474
|
+
codeLink: codeLinkFactsSchema
|
|
16475
|
+
}).strict().superRefine((v, ctx) => checkPartsUnique(v.parts, ctx, ["parts"]));
|
|
16476
|
+
var observedSaidSchema = external_exports.object({
|
|
16477
|
+
title: buildTaskSchema.shape.title,
|
|
16478
|
+
agent: buildTaskSchema.shape.agent,
|
|
16479
|
+
whyOutsidePlan: buildTaskSchema.shape.whyOutsidePlan
|
|
16480
|
+
}).strict();
|
|
16481
|
+
var observedNotInPlanSchema = external_exports.object({
|
|
16482
|
+
part: systemPartIdSchema,
|
|
16483
|
+
name: systemPartSchema.shape.name,
|
|
16484
|
+
said: external_exports.array(observedSaidSchema).max(BUILD_SNAPSHOT_TASK_LIMIT)
|
|
16485
|
+
}).strict();
|
|
16486
|
+
var observedChangesRecordedSchema = external_exports.object({
|
|
16487
|
+
outcome: external_exports.literal("recorded"),
|
|
16488
|
+
notInPlan: external_exports.array(observedNotInPlanSchema).max(SYSTEM_PARTS_MAX).superRefine((parts, ctx) => checkPartsUnique(parts, ctx, [])),
|
|
16489
|
+
outside: external_exports.object({
|
|
16490
|
+
files: observedCountSchema,
|
|
16491
|
+
example: observedPathSchema.nullable()
|
|
16492
|
+
}).strict().refine((v) => v.example === null || v.files >= 1, {
|
|
16493
|
+
message: "names an example when no file changed",
|
|
16494
|
+
path: ["example"]
|
|
16495
|
+
})
|
|
16496
|
+
}).strict();
|
|
16497
|
+
var observedChangesKeptSchema = external_exports.object({
|
|
16498
|
+
outcome: external_exports.literal("kept")
|
|
16499
|
+
}).strict();
|
|
16500
|
+
var observedChangesReportResultSchema = external_exports.discriminatedUnion("outcome", [
|
|
16501
|
+
observedChangesRecordedSchema,
|
|
16502
|
+
observedChangesKeptSchema
|
|
16503
|
+
]);
|
|
16504
|
+
var OBSERVE_NOT_SENT_REASONS = [
|
|
16505
|
+
"not-configured",
|
|
16506
|
+
"nothing-to-count",
|
|
16507
|
+
"cannot-count",
|
|
16508
|
+
"start-not-here",
|
|
16509
|
+
"start-not-on-branch",
|
|
16510
|
+
"basis-moved",
|
|
16511
|
+
"refused",
|
|
16512
|
+
"unreachable"
|
|
16513
|
+
];
|
|
16514
|
+
var observeRunRecordedSchema = external_exports.object({ outcome: external_exports.literal("recorded") }).strict();
|
|
16515
|
+
var observeRunKeptSchema = external_exports.object({ outcome: external_exports.literal("kept") }).strict();
|
|
16516
|
+
var observeRunNotSentSchema = external_exports.object({
|
|
16517
|
+
outcome: external_exports.literal("not-sent"),
|
|
16518
|
+
reason: external_exports.enum(OBSERVE_NOT_SENT_REASONS)
|
|
16519
|
+
}).strict();
|
|
16520
|
+
var observeRunResultSchema = external_exports.discriminatedUnion("outcome", [
|
|
16521
|
+
observeRunRecordedSchema,
|
|
16522
|
+
observeRunKeptSchema,
|
|
16523
|
+
observeRunNotSentSchema
|
|
16524
|
+
]);
|
|
16525
|
+
var observedChecksFiredSchema = external_exports.array(external_exports.string().min(1)).refine((names) => names.every((name, i) => i === 0 || names[i - 1] < name), {
|
|
16526
|
+
message: "must be sorted, each name once"
|
|
16527
|
+
});
|
|
16528
|
+
var observedCountingSchema = external_exports.discriminatedUnion("state", [
|
|
16529
|
+
external_exports.object({
|
|
16530
|
+
state: external_exports.literal("counted"),
|
|
16531
|
+
startCommit: closeCommitSchema,
|
|
16532
|
+
startRecordedAt: utcIso8601,
|
|
16533
|
+
headCommit: closeCommitSchema,
|
|
16534
|
+
headCommittedAt: utcIso8601,
|
|
16535
|
+
commits: observedCountSchema,
|
|
16536
|
+
countedAt: utcIso8601
|
|
16537
|
+
}).strict(),
|
|
16538
|
+
external_exports.object({
|
|
16539
|
+
state: external_exports.literal("not-counted-yet"),
|
|
16540
|
+
startCommit: closeCommitSchema,
|
|
16541
|
+
startRecordedAt: utcIso8601
|
|
16542
|
+
}).strict(),
|
|
16543
|
+
external_exports.object({
|
|
16544
|
+
state: external_exports.literal("cannot-count")
|
|
16545
|
+
}).strict()
|
|
16546
|
+
]);
|
|
16547
|
+
var observedPartSchema = external_exports.object({
|
|
16548
|
+
part: systemPartIdSchema,
|
|
16549
|
+
files: observedFilesSchema.nullable(),
|
|
16550
|
+
checksFired: observedChecksFiredSchema
|
|
16551
|
+
}).strict().refine((v) => v.files !== null && filesChanged(v.files) >= 1 || v.checksFired.length >= 1, {
|
|
16552
|
+
message: "a part is listed only when files changed under it or a check fired on one",
|
|
16553
|
+
path: ["files"]
|
|
16554
|
+
});
|
|
16555
|
+
var observedOutsideSchema = external_exports.object({
|
|
16556
|
+
files: observedFilesSchema.nullable(),
|
|
16557
|
+
paths: observedPathListSchema,
|
|
16558
|
+
checksFired: observedChecksFiredSchema
|
|
16559
|
+
}).strict().refine((v) => pathsFitTheCount(v.files, v.paths), {
|
|
16560
|
+
message: "lists more paths than files changed",
|
|
16561
|
+
path: ["paths"]
|
|
16562
|
+
});
|
|
16563
|
+
var observedChangesSchema = external_exports.object({
|
|
16564
|
+
phase: phaseNameSchema,
|
|
16565
|
+
counting: observedCountingSchema,
|
|
16566
|
+
descriptionVersion: observedDescriptionVersionSchema.nullable(),
|
|
16567
|
+
parts: external_exports.array(observedPartSchema).max(SYSTEM_PARTS_MAX),
|
|
16568
|
+
methodFiles: observedMethodFilesSchema.nullable(),
|
|
16569
|
+
outsideEveryPart: observedOutsideSchema,
|
|
16570
|
+
codeLink: codeLinkFactsSchema.nullable()
|
|
16571
|
+
}).strict().superRefine((v, ctx) => {
|
|
16572
|
+
checkPartsUnique(v.parts, ctx, ["parts"]);
|
|
16573
|
+
const counted = v.counting.state === "counted";
|
|
16574
|
+
const rule = counted ? "must be present when the phase is counted" : "must be null until the phase is counted";
|
|
16575
|
+
if (v.methodFiles !== null !== counted)
|
|
16576
|
+
ctx.addIssue({ code: "custom", path: ["methodFiles"], message: rule });
|
|
16577
|
+
if (v.outsideEveryPart.files !== null !== counted) {
|
|
16578
|
+
ctx.addIssue({ code: "custom", path: ["outsideEveryPart", "files"], message: rule });
|
|
16579
|
+
}
|
|
16580
|
+
if (v.codeLink !== null !== counted)
|
|
16581
|
+
ctx.addIssue({ code: "custom", path: ["codeLink"], message: rule });
|
|
16582
|
+
v.parts.forEach((entry, i) => {
|
|
16583
|
+
if (entry.files !== null !== counted)
|
|
16584
|
+
ctx.addIssue({ code: "custom", path: ["parts", i, "files"], message: rule });
|
|
16585
|
+
});
|
|
16586
|
+
if (counted && v.descriptionVersion === null) {
|
|
16587
|
+
ctx.addIssue({ code: "custom", path: ["descriptionVersion"], message: "must name the version the counts used" });
|
|
16588
|
+
}
|
|
16589
|
+
});
|
|
16590
|
+
var partOutsideThePlanSchema = external_exports.object({
|
|
16591
|
+
part: systemPartIdSchema,
|
|
16592
|
+
name: systemPartSchema.shape.name,
|
|
16593
|
+
seen: observedFilesSchema.nullable(),
|
|
16594
|
+
tasks: external_exports.array(observedSaidSchema.extend({ id: buildTaskSchema.shape.id }).strict()).max(BUILD_SNAPSHOT_TASK_LIMIT)
|
|
16595
|
+
}).strict();
|
|
16596
|
+
var NO_TASK_SAID_LINE = "No task said it changed this part.";
|
|
16597
|
+
function agentSaid(task) {
|
|
16598
|
+
const { title, agent, whyOutsidePlan } = task;
|
|
16599
|
+
if (whyOutsidePlan === void 0)
|
|
16600
|
+
return `"${title}" said it built this part, and gave no reason.`;
|
|
16601
|
+
if (agent === HUMAN_AGENT)
|
|
16602
|
+
return `The person on "${title}" said: "${whyOutsidePlan}"`;
|
|
16603
|
+
if (agent === void 0)
|
|
16604
|
+
return `The agent on "${title}" said: "${whyOutsidePlan}"`;
|
|
16605
|
+
return `${agent} said, on "${title}": "${whyOutsidePlan}"`;
|
|
16606
|
+
}
|
|
16607
|
+
|
|
16608
|
+
// ../events/dist/phase-review.js
|
|
16609
|
+
var BUILT_PART_ACTS = Object.freeze(["accept", "question", "send-back"]);
|
|
16610
|
+
var NOT_BUILT_PART_ACTS = Object.freeze(["carry", "drop"]);
|
|
16611
|
+
var REVIEW_ACTS = Object.freeze([...BUILT_PART_ACTS, ...NOT_BUILT_PART_ACTS]);
|
|
16612
|
+
var reviewActSchema = external_exports.enum(REVIEW_ACTS);
|
|
16613
|
+
function reviewActsFor(built) {
|
|
16614
|
+
return built ? BUILT_PART_ACTS : NOT_BUILT_PART_ACTS;
|
|
16615
|
+
}
|
|
16616
|
+
var REVIEW_NOTE_MAX = SCOPE_NOTE_MAX;
|
|
16617
|
+
var reviewNoteWireSchema = external_exports.string().max(REVIEW_NOTE_MAX);
|
|
16618
|
+
var reviewNoteSchema = external_exports.string().min(1).max(REVIEW_NOTE_MAX);
|
|
16619
|
+
var REVIEW_ANSWER_MAX = 1e3;
|
|
16620
|
+
var reviewAgentAnswerSchema = external_exports.string().min(1).max(REVIEW_ANSWER_MAX);
|
|
16621
|
+
var REVIEW_PARTS_MAX = PHASE_PLAN_CHANGES_MAX * 2;
|
|
16622
|
+
var REVIEW_PART_CHANGES = Object.freeze([...PHASE_PLAN_CHANGE_KINDS, "not-in-plan"]);
|
|
16623
|
+
var reviewPartChangeSchema = external_exports.enum(REVIEW_PART_CHANGES);
|
|
16624
|
+
var REVIEW_DECISIONS = Object.freeze(["accepted", "carried", "dropped"]);
|
|
16625
|
+
var reviewDecisionSchema = external_exports.enum(REVIEW_DECISIONS);
|
|
16626
|
+
function actOf(decision) {
|
|
16627
|
+
if (decision === "accepted")
|
|
16628
|
+
return "accept";
|
|
16629
|
+
if (decision === "carried")
|
|
16630
|
+
return "carry";
|
|
16631
|
+
return "drop";
|
|
16632
|
+
}
|
|
16633
|
+
var reviewCarriedToSchema = carriedPhaseSchema.nullable();
|
|
16634
|
+
var reviewStepNameSchema = stepNameSchema.refine((v) => !stepIdSchema.safeParse(v.trim()).success, {
|
|
16635
|
+
message: "must be the step's plain name"
|
|
16636
|
+
});
|
|
16637
|
+
var reviewStepSchema = external_exports.object({
|
|
16638
|
+
name: reviewStepNameSchema.nullable(),
|
|
16639
|
+
state: unfinishedStepSchema.shape.state,
|
|
16640
|
+
judged: external_exports.boolean()
|
|
16641
|
+
}).strict();
|
|
16642
|
+
var UNNAMED_STEP = "a step of this phase";
|
|
16643
|
+
function stepCalled(step) {
|
|
16644
|
+
return step.name === null ? UNNAMED_STEP : `"${step.name}"`;
|
|
16645
|
+
}
|
|
16646
|
+
function sentenceStart(text) {
|
|
16647
|
+
return text.charAt(0).toUpperCase() + text.slice(1);
|
|
16648
|
+
}
|
|
16649
|
+
function unfinishedStepLabel(step) {
|
|
16650
|
+
const state = step.state === "parked" ? "parked" : "attempted, and did not finish";
|
|
16651
|
+
return `${stepCalled(step)} (${state})`;
|
|
16652
|
+
}
|
|
16653
|
+
function unfinishedStepLine(step) {
|
|
16654
|
+
if (step.judged) {
|
|
16655
|
+
return `${sentenceStart(stepCalled(step))} is judged by Halfcycle and has not passed in this phase: run that review again in your coding session until it passes.`;
|
|
16656
|
+
}
|
|
16657
|
+
return `Finish ${stepCalled(step)} in your coding session, or record it as declined.`;
|
|
16658
|
+
}
|
|
16659
|
+
var openReviewRequestSchema = external_exports.object({
|
|
16660
|
+
actor: closeActorSchema,
|
|
16661
|
+
acceptanceOutcome: acceptanceOutcomeSchema,
|
|
16662
|
+
statements: closeStatementsSchema
|
|
16663
|
+
}).strict();
|
|
16664
|
+
var REVIEW_OPEN_TASKS_SHOWN_MAX = 10;
|
|
16665
|
+
var reviewOpenedSchema = external_exports.object({
|
|
16666
|
+
outcome: external_exports.literal("opened"),
|
|
16667
|
+
round: external_exports.number().int().min(1),
|
|
16668
|
+
parts: external_exports.number().int().min(0).max(REVIEW_PARTS_MAX),
|
|
16669
|
+
notBuilt: external_exports.number().int().min(0).max(REVIEW_PARTS_MAX),
|
|
16670
|
+
counted: external_exports.boolean(),
|
|
16671
|
+
typedReviewOffered: external_exports.boolean()
|
|
16672
|
+
}).strict().refine((v) => v.notBuilt <= v.parts, { message: "cannot exceed the parts in the round", path: ["notBuilt"] });
|
|
16673
|
+
var reviewTasksOpenSchema = external_exports.object({
|
|
16674
|
+
outcome: external_exports.literal("tasks-open"),
|
|
16675
|
+
open: external_exports.array(buildTaskSchema.shape.title).min(1).max(REVIEW_OPEN_TASKS_SHOWN_MAX),
|
|
16676
|
+
more: external_exports.number().int().min(0).max(BUILD_SNAPSHOT_TASK_LIMIT)
|
|
16677
|
+
}).strict();
|
|
16678
|
+
var reviewOutstandingSchema = external_exports.object({
|
|
16679
|
+
outcome: external_exports.literal("outstanding"),
|
|
16680
|
+
steps: external_exports.array(reviewStepSchema).min(1)
|
|
16681
|
+
}).strict();
|
|
16682
|
+
var reviewCleanOverFindingsSchema = external_exports.object({
|
|
16683
|
+
outcome: external_exports.literal("clean-over-findings"),
|
|
16684
|
+
findings: external_exports.number().int().min(1)
|
|
16685
|
+
}).strict();
|
|
16686
|
+
var reviewRefusedSchema = external_exports.object({
|
|
16687
|
+
outcome: external_exports.literal("refused"),
|
|
16688
|
+
problems: external_exports.array(systemDescriptionProblemSchema).min(1)
|
|
16689
|
+
}).strict();
|
|
16690
|
+
function bareReviewOutcome(outcome) {
|
|
16691
|
+
return external_exports.object({ outcome: external_exports.literal(outcome) }).strict();
|
|
16692
|
+
}
|
|
16693
|
+
var openReviewResultSchema = external_exports.discriminatedUnion("outcome", [
|
|
16694
|
+
reviewOpenedSchema,
|
|
16695
|
+
reviewTasksOpenSchema,
|
|
16696
|
+
bareReviewOutcome("no-build"),
|
|
16697
|
+
bareReviewOutcome("review-open"),
|
|
16698
|
+
reviewOutstandingSchema,
|
|
16699
|
+
reviewCleanOverFindingsSchema,
|
|
16700
|
+
reviewRefusedSchema,
|
|
16701
|
+
bareReviewOutcome("unavailable")
|
|
16702
|
+
]);
|
|
16703
|
+
var reviewPhaseNotOpenSchema = external_exports.object({
|
|
16704
|
+
statusCode: external_exports.literal(409),
|
|
16705
|
+
error: external_exports.literal("PhaseNotOpen"),
|
|
16706
|
+
message: external_exports.string().min(1),
|
|
16707
|
+
openPhase: external_exports.string().nullable()
|
|
16708
|
+
}).strict();
|
|
16709
|
+
var openReviewAnswerSchema = external_exports.union([openReviewResultSchema, reviewPhaseNotOpenSchema]);
|
|
16710
|
+
var reviewDecisionEntrySchema = external_exports.object({
|
|
16711
|
+
part: systemPartIdSchema,
|
|
16712
|
+
act: reviewActSchema,
|
|
16713
|
+
note: reviewNoteWireSchema.optional()
|
|
16714
|
+
}).strict();
|
|
16715
|
+
var reviewDecisionRequestSchema = external_exports.object({
|
|
16716
|
+
reviewVersion: external_exports.number().int().min(0),
|
|
16717
|
+
decisions: external_exports.array(reviewDecisionEntrySchema).max(REVIEW_PARTS_MAX)
|
|
16718
|
+
}).strict();
|
|
16719
|
+
var reviewDecidedSchema = external_exports.object({
|
|
16720
|
+
part: systemPartIdSchema,
|
|
16721
|
+
name: systemPartSchema.shape.name,
|
|
16722
|
+
act: reviewActSchema,
|
|
16723
|
+
carriedTo: reviewCarriedToSchema.optional()
|
|
16724
|
+
}).strict().refine((v) => v.carriedTo !== void 0 === (v.act === "carry"), {
|
|
16725
|
+
message: "is present exactly on a carry",
|
|
16726
|
+
path: ["carriedTo"]
|
|
16727
|
+
});
|
|
16728
|
+
var reviewRecordedSchema = external_exports.object({
|
|
16729
|
+
outcome: external_exports.literal("recorded"),
|
|
16730
|
+
decided: external_exports.array(reviewDecidedSchema).max(REVIEW_PARTS_MAX),
|
|
16731
|
+
waiting: external_exports.number().int().min(0).max(REVIEW_PARTS_MAX),
|
|
16732
|
+
closed: external_exports.boolean(),
|
|
16733
|
+
reviewVersion: external_exports.number().int().min(1)
|
|
16734
|
+
}).strict().superRefine((v, ctx) => {
|
|
16735
|
+
if (v.closed && v.waiting !== 0) {
|
|
16736
|
+
ctx.addIssue({ code: "custom", path: ["waiting"], message: "must be 0 once the phase is closed" });
|
|
16737
|
+
}
|
|
16738
|
+
if (v.decided.length === 0 && !v.closed) {
|
|
16739
|
+
ctx.addIssue({ code: "custom", path: ["decided"], message: "may be empty only when the phase closed" });
|
|
16740
|
+
}
|
|
16741
|
+
});
|
|
16742
|
+
var REVIEW_NOT_DECIDABLE_WHY = Object.freeze(["settled", "question-waiting", "sent-back"]);
|
|
16743
|
+
var reviewNotDecidablePartSchema = external_exports.object({
|
|
16744
|
+
part: systemPartIdSchema,
|
|
16745
|
+
name: systemPartSchema.shape.name,
|
|
16746
|
+
why: external_exports.enum(REVIEW_NOT_DECIDABLE_WHY)
|
|
16747
|
+
}).strict();
|
|
16748
|
+
var reviewNotDecidableSchema = external_exports.object({
|
|
16749
|
+
outcome: external_exports.literal("not-decidable"),
|
|
16750
|
+
parts: external_exports.array(reviewNotDecidablePartSchema).min(1).max(REVIEW_PARTS_MAX)
|
|
16751
|
+
}).strict();
|
|
16752
|
+
var reviewCloseRefusedSchema = external_exports.object({
|
|
16753
|
+
outcome: external_exports.literal("close-refused"),
|
|
16754
|
+
steps: external_exports.array(reviewStepSchema)
|
|
16755
|
+
}).strict();
|
|
16756
|
+
var reviewWalkChangedSchema = external_exports.object({
|
|
16757
|
+
outcome: external_exports.literal("walk-changed"),
|
|
16758
|
+
findings: external_exports.number().int().min(1)
|
|
16759
|
+
}).strict();
|
|
16760
|
+
var reviewDecisionResultSchema = external_exports.discriminatedUnion("outcome", [
|
|
16761
|
+
reviewRecordedSchema,
|
|
16762
|
+
bareReviewOutcome("review-changed"),
|
|
16763
|
+
bareReviewOutcome("no-review"),
|
|
16764
|
+
reviewNotDecidableSchema,
|
|
16765
|
+
reviewCloseRefusedSchema,
|
|
16766
|
+
reviewWalkChangedSchema,
|
|
16767
|
+
bareReviewOutcome("not-owner"),
|
|
16768
|
+
bareReviewOutcome("no-owner"),
|
|
16769
|
+
reviewRefusedSchema,
|
|
16770
|
+
bareReviewOutcome("unavailable")
|
|
16771
|
+
]);
|
|
16772
|
+
var reviewSentBackSchema = external_exports.object({
|
|
16773
|
+
round: external_exports.number().int().min(1),
|
|
16774
|
+
note: reviewNoteSchema
|
|
16775
|
+
}).strict();
|
|
16776
|
+
var reviewQuestionSchema = external_exports.object({
|
|
16777
|
+
question: reviewNoteSchema,
|
|
16778
|
+
agentAnswer: reviewAgentAnswerSchema.optional()
|
|
16779
|
+
}).strict();
|
|
16780
|
+
var reviewTaskSchema = external_exports.object({
|
|
16781
|
+
title: buildTaskSchema.shape.title,
|
|
16782
|
+
state: buildTaskStateSchema
|
|
16783
|
+
}).strict();
|
|
16784
|
+
var reviewViewPartSchema = external_exports.object({
|
|
16785
|
+
part: systemPartIdSchema,
|
|
16786
|
+
name: systemPartSchema.shape.name,
|
|
16787
|
+
change: reviewPartChangeSchema,
|
|
16788
|
+
built: external_exports.boolean(),
|
|
16789
|
+
builtWithoutApproval: external_exports.boolean(),
|
|
16790
|
+
decidable: external_exports.boolean(),
|
|
16791
|
+
waiting: external_exports.boolean(),
|
|
16792
|
+
decision: reviewDecisionSchema.optional(),
|
|
16793
|
+
reason: reviewNoteSchema.optional(),
|
|
16794
|
+
carriedTo: reviewCarriedToSchema.optional(),
|
|
16795
|
+
sentBack: external_exports.array(reviewSentBackSchema),
|
|
16796
|
+
questions: external_exports.array(reviewQuestionSchema),
|
|
16797
|
+
tasks: external_exports.array(reviewTaskSchema).max(BUILD_SNAPSHOT_TASK_LIMIT),
|
|
16798
|
+
said: external_exports.array(observedSaidSchema).max(BUILD_SNAPSHOT_TASK_LIMIT),
|
|
16799
|
+
codeLink: partCodeLinkSchema
|
|
16800
|
+
}).strict().superRefine((v, ctx) => {
|
|
16801
|
+
if (v.waiting !== (v.decision === void 0)) {
|
|
16802
|
+
ctx.addIssue({ code: "custom", path: ["waiting"], message: "must be true exactly when the part has no decision" });
|
|
16803
|
+
}
|
|
16804
|
+
if (v.decidable && !v.waiting) {
|
|
16805
|
+
ctx.addIssue({ code: "custom", path: ["decidable"], message: "cannot be true for a settled part" });
|
|
16806
|
+
}
|
|
16807
|
+
if (v.decision !== void 0 && !reviewActsFor(v.built).includes(actOf(v.decision))) {
|
|
16808
|
+
ctx.addIssue({ code: "custom", path: ["decision"], message: "is not an act this part takes" });
|
|
16809
|
+
}
|
|
16810
|
+
const carriedOrDropped = v.decision === "carried" || v.decision === "dropped";
|
|
16811
|
+
if (v.reason !== void 0 !== carriedOrDropped) {
|
|
16812
|
+
ctx.addIssue({ code: "custom", path: ["reason"], message: "is present exactly on a carry or a drop" });
|
|
16813
|
+
}
|
|
16814
|
+
if (v.carriedTo !== void 0 !== (v.decision === "carried")) {
|
|
16815
|
+
ctx.addIssue({ code: "custom", path: ["carriedTo"], message: "is present exactly on a carry" });
|
|
16816
|
+
}
|
|
16817
|
+
if (v.change === "not-in-plan" && !v.built) {
|
|
16818
|
+
ctx.addIssue({ code: "custom", path: ["built"], message: "is always true for a part outside the plan" });
|
|
16819
|
+
}
|
|
16820
|
+
});
|
|
16821
|
+
var reviewPhaseNumberSchema = external_exports.number().int().min(1).nullable();
|
|
16822
|
+
var phaseReviewNoneSchema = external_exports.object({
|
|
16823
|
+
state: external_exports.literal("none"),
|
|
16824
|
+
phase: scopePhaseSchema,
|
|
16825
|
+
number: reviewPhaseNumberSchema,
|
|
16826
|
+
closedWithoutReview: external_exports.boolean()
|
|
16827
|
+
}).strict();
|
|
16828
|
+
var reviewWalkSchema = external_exports.object({
|
|
16829
|
+
verdict: walkVerdictSchema,
|
|
16830
|
+
findings: external_exports.number().int().min(0)
|
|
16831
|
+
}).strict();
|
|
16832
|
+
function reviewedViewSchema(state) {
|
|
16833
|
+
return external_exports.object({
|
|
16834
|
+
state: external_exports.literal(state),
|
|
16835
|
+
phase: scopePhaseSchema,
|
|
16836
|
+
number: reviewPhaseNumberSchema,
|
|
16837
|
+
cycle: external_exports.number().int().min(0),
|
|
16838
|
+
round: external_exports.number().int().min(1),
|
|
16839
|
+
reviewVersion: external_exports.number().int().min(1),
|
|
16840
|
+
typedReviewOffered: external_exports.boolean(),
|
|
16841
|
+
counted: external_exports.boolean(),
|
|
16842
|
+
walk: reviewWalkSchema,
|
|
16843
|
+
parts: external_exports.array(reviewViewPartSchema).max(REVIEW_PARTS_MAX),
|
|
16844
|
+
observed: observedChangesSchema.nullable(),
|
|
16845
|
+
description: systemDescriptionSchema
|
|
16846
|
+
}).strict().superRefine((v, ctx) => {
|
|
16847
|
+
const seen = /* @__PURE__ */ new Set();
|
|
16848
|
+
v.parts.forEach((p, i) => {
|
|
16849
|
+
if (seen.has(p.part))
|
|
16850
|
+
ctx.addIssue({ code: "custom", path: ["parts", i, "part"], message: "is listed twice" });
|
|
16851
|
+
seen.add(p.part);
|
|
16852
|
+
});
|
|
16853
|
+
const counted = v.observed !== null && v.observed.counting.state === "counted";
|
|
16854
|
+
if (v.counted !== counted) {
|
|
16855
|
+
ctx.addIssue({ code: "custom", path: ["counted"], message: "must be true exactly when the counts are counted" });
|
|
16856
|
+
}
|
|
16857
|
+
if (v.observed !== null && v.observed.phase !== v.phase) {
|
|
16858
|
+
ctx.addIssue({ code: "custom", path: ["observed", "phase"], message: "must be the phase under review" });
|
|
16859
|
+
}
|
|
16860
|
+
if (v.state === "closed") {
|
|
16861
|
+
v.parts.forEach((p, i) => {
|
|
16862
|
+
if (p.decision === void 0) {
|
|
16863
|
+
ctx.addIssue({ code: "custom", path: ["parts", i, "decision"], message: "every part of a closed review is decided" });
|
|
16864
|
+
}
|
|
16865
|
+
});
|
|
16866
|
+
}
|
|
16867
|
+
});
|
|
16868
|
+
}
|
|
16869
|
+
var phaseReviewOpenSchema = reviewedViewSchema("open");
|
|
16870
|
+
var phaseReviewClosedSchema = reviewedViewSchema("closed");
|
|
16871
|
+
var phaseReviewViewSchema = external_exports.discriminatedUnion("state", [
|
|
16872
|
+
phaseReviewNoneSchema,
|
|
16873
|
+
phaseReviewOpenSchema,
|
|
16874
|
+
phaseReviewClosedSchema
|
|
16875
|
+
]);
|
|
16876
|
+
|
|
16019
16877
|
// ../events/dist/client-version.js
|
|
16020
16878
|
var CLIENT_VERSION_HEADER = "x-halfcycle-client";
|
|
16021
16879
|
var TYPED_APPROVAL_OFF_SENTENCE = "Approving by typing is switched off for now.";
|
|
16880
|
+
var TYPED_REVIEW_OFF_SENTENCE = "Reviewing by typing is switched off for now.";
|
|
16022
16881
|
var CLIENT_UPDATE_SENTENCE = `To approve by typing in a session, Halfcycle needs updating: run \`${HALFCYCLE_COMMAND}\` in this project's folder, then reopen your session and ask again.`;
|
|
16023
16882
|
|
|
16024
16883
|
// ../events/dist/scope-notices.js
|
|
@@ -16068,7 +16927,7 @@ function sentBackNotice(entry) {
|
|
|
16068
16927
|
return [
|
|
16069
16928
|
`Before anything else: the project's owner sent the scope of phase ${entry.number}, "${entry.phase}", back with these notes.`,
|
|
16070
16929
|
...entry.notes.map((n) => n.part === void 0 ? `- ${n.note}` : `- "${n.part}": ${n.note}`),
|
|
16071
|
-
`Change the plan to answer every note, then send it again
|
|
16930
|
+
`Change the plan to answer every note, then send it again: say "plan the next phase" to start. It then waits for the owner's approval again.`
|
|
16072
16931
|
].join("\n");
|
|
16073
16932
|
}
|
|
16074
16933
|
function waitingNotice(entry, offerTyping) {
|
|
@@ -16086,6 +16945,146 @@ function scopeNoticeText(scope, options) {
|
|
|
16086
16945
|
};
|
|
16087
16946
|
}
|
|
16088
16947
|
|
|
16948
|
+
// ../events/dist/review-notices.js
|
|
16949
|
+
var reviewNoticeQuestionSchema = external_exports.object({
|
|
16950
|
+
phase: scopePhaseSchema,
|
|
16951
|
+
number: reviewPhaseNumberSchema,
|
|
16952
|
+
part: systemPartIdSchema,
|
|
16953
|
+
partName: systemPartSchema.shape.name,
|
|
16954
|
+
question: reviewNoteSchema
|
|
16955
|
+
}).strict();
|
|
16956
|
+
var reviewNoticeSentBackSchema = external_exports.object({
|
|
16957
|
+
phase: scopePhaseSchema,
|
|
16958
|
+
number: reviewPhaseNumberSchema,
|
|
16959
|
+
part: systemPartIdSchema,
|
|
16960
|
+
partName: systemPartSchema.shape.name,
|
|
16961
|
+
note: reviewNoteSchema
|
|
16962
|
+
}).strict();
|
|
16963
|
+
var reviewNoticesSchema = external_exports.object({
|
|
16964
|
+
questions: external_exports.array(reviewNoticeQuestionSchema).max(REVIEW_PARTS_MAX),
|
|
16965
|
+
sentBack: external_exports.array(reviewNoticeSentBackSchema).max(REVIEW_PARTS_MAX)
|
|
16966
|
+
}).strict().refine((v) => v.questions.length > 0 || v.sentBack.length > 0, {
|
|
16967
|
+
message: "carried only when a question waits or a part was sent back"
|
|
16968
|
+
});
|
|
16969
|
+
function phaseCalled(phase, number4) {
|
|
16970
|
+
return number4 === null ? `phase "${phase}"` : `phase ${number4}, "${phase}"`;
|
|
16971
|
+
}
|
|
16972
|
+
function questionNotice(entry) {
|
|
16973
|
+
const answer = `${HALFCYCLE_COMMAND} review ${quoteForShell(entry.phase)} --answer ${quoteForShell(entry.part)} "<your answer>"`;
|
|
16974
|
+
return `Before anything else: the project's owner asked about "${entry.partName}" in the review of ${phaseCalled(entry.phase, entry.number)}: ${entry.question} Answer it with ${answer}.`;
|
|
16975
|
+
}
|
|
16976
|
+
function sentBackNotice2(entry) {
|
|
16977
|
+
const where = entry.number === null ? `in phase "${entry.phase}"` : `in phase ${entry.number}, "${entry.phase}",`;
|
|
16978
|
+
return `Before anything else: the project's owner sent "${entry.partName}" ${where} back to building: ${entry.note} Add or reopen the tasks that build it. When they are finished, run the close step again.`;
|
|
16979
|
+
}
|
|
16980
|
+
function reviewNoticeText(review) {
|
|
16981
|
+
return [...review.questions.map(questionNotice), ...review.sentBack.map(sentBackNotice2)];
|
|
16982
|
+
}
|
|
16983
|
+
|
|
16984
|
+
// ../events/dist/device-auth.js
|
|
16985
|
+
var utcIso86012 = external_exports.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/, "must be a UTC ISO-8601 timestamp ending in Z");
|
|
16986
|
+
var DEVICE_AUTH_STATUS = {
|
|
16987
|
+
/** The poll: minted, nobody has acted yet. Keep polling. 200. */
|
|
16988
|
+
PENDING: "pending",
|
|
16989
|
+
/** The poll: approved, and the credential is in this body. Once only. 200. */
|
|
16990
|
+
APPROVED: "approved",
|
|
16991
|
+
/** Confirm: the decision was recorded (approve or decline). 200. */
|
|
16992
|
+
RECORDED: "recorded",
|
|
16993
|
+
/** The human refused at the confirmation page. 400 on the poll. */
|
|
16994
|
+
DECLINED: "declined",
|
|
16995
|
+
/** The code's absolute expiry has passed. 400. */
|
|
16996
|
+
EXPIRED: "expired",
|
|
16997
|
+
/** Never minted, or already spent. 400. */
|
|
16998
|
+
UNKNOWN: "unknown",
|
|
16999
|
+
/** Confirm: neither `approve` nor `decline` was named. Nothing was bound. 400. */
|
|
17000
|
+
INVALID_DECISION: "invalid-decision",
|
|
17001
|
+
/** Confirm: an approval arrived with no signed-in identity. 400. */
|
|
17002
|
+
IDENTITY_REQUIRED: "identity-required",
|
|
17003
|
+
/** This deployment has no `SIGNIN_BASE_URL` / `DEVICE_AUTH_CONFIRM_SECRET`. 503. */
|
|
17004
|
+
NOT_CONFIGURED: "not-configured",
|
|
17005
|
+
/** Confirm: the shared confirmation secret was missing or wrong. 401. */
|
|
17006
|
+
UNAUTHORIZED: "unauthorized",
|
|
17007
|
+
/** Confirm: the code was already approved, declined or claimed. 409. */
|
|
17008
|
+
ALREADY_DECIDED: "already-decided",
|
|
17009
|
+
/**
|
|
17010
|
+
* Confirm: an approval named a signed-in identity whose account has not accepted
|
|
17011
|
+
* the terms of service and privacy policy currently in force. 400.
|
|
17012
|
+
*
|
|
17013
|
+
* NOT BOUND TO THE DEVICE CODE. Unlike every other refusal in this const, this one
|
|
17014
|
+
* says nothing about the code itself — the code is still `pending` after this
|
|
17015
|
+
* response, exactly as it was before the request, so the SAME userCode can be
|
|
17016
|
+
* confirmed again once the caller has recorded acceptance. `deviceConfirmRequestSchema`'s
|
|
17017
|
+
* `acceptTerms` is how a second confirm says it did.
|
|
17018
|
+
*/
|
|
17019
|
+
TERMS_REQUIRED: "terms-required"
|
|
17020
|
+
};
|
|
17021
|
+
var DEVICE_POLL_REFUSALS = [
|
|
17022
|
+
DEVICE_AUTH_STATUS.DECLINED,
|
|
17023
|
+
DEVICE_AUTH_STATUS.EXPIRED,
|
|
17024
|
+
DEVICE_AUTH_STATUS.UNKNOWN
|
|
17025
|
+
];
|
|
17026
|
+
var deviceAuthStartResponseSchema = external_exports.object({
|
|
17027
|
+
deviceCode: external_exports.string().min(1),
|
|
17028
|
+
userCode: external_exports.string().min(1),
|
|
17029
|
+
verificationUrl: external_exports.string().min(1),
|
|
17030
|
+
expiresAt: utcIso86012,
|
|
17031
|
+
pollIntervalMs: external_exports.number().int().positive()
|
|
17032
|
+
}).strict();
|
|
17033
|
+
var devicePollPendingSchema = external_exports.object({
|
|
17034
|
+
status: external_exports.literal(DEVICE_AUTH_STATUS.PENDING),
|
|
17035
|
+
pollIntervalMs: external_exports.number().int().positive()
|
|
17036
|
+
}).strict();
|
|
17037
|
+
var devicePollApprovedSchema = external_exports.object({
|
|
17038
|
+
status: external_exports.literal(DEVICE_AUTH_STATUS.APPROVED),
|
|
17039
|
+
accountId: external_exports.string().min(1),
|
|
17040
|
+
credential: external_exports.string().min(1),
|
|
17041
|
+
expiresAt: utcIso86012
|
|
17042
|
+
}).strict();
|
|
17043
|
+
var deviceAuthRefusalSchema = external_exports.object({
|
|
17044
|
+
status: external_exports.enum([
|
|
17045
|
+
DEVICE_AUTH_STATUS.DECLINED,
|
|
17046
|
+
DEVICE_AUTH_STATUS.EXPIRED,
|
|
17047
|
+
DEVICE_AUTH_STATUS.UNKNOWN,
|
|
17048
|
+
DEVICE_AUTH_STATUS.INVALID_DECISION,
|
|
17049
|
+
DEVICE_AUTH_STATUS.IDENTITY_REQUIRED,
|
|
17050
|
+
DEVICE_AUTH_STATUS.NOT_CONFIGURED,
|
|
17051
|
+
DEVICE_AUTH_STATUS.UNAUTHORIZED,
|
|
17052
|
+
DEVICE_AUTH_STATUS.ALREADY_DECIDED,
|
|
17053
|
+
DEVICE_AUTH_STATUS.TERMS_REQUIRED
|
|
17054
|
+
]),
|
|
17055
|
+
message: external_exports.string().min(1)
|
|
17056
|
+
}).strict();
|
|
17057
|
+
var devicePollResponseSchema = external_exports.union([
|
|
17058
|
+
devicePollPendingSchema,
|
|
17059
|
+
devicePollApprovedSchema,
|
|
17060
|
+
deviceAuthRefusalSchema
|
|
17061
|
+
]);
|
|
17062
|
+
var deviceConfirmRequestSchema = external_exports.object({
|
|
17063
|
+
userCode: external_exports.string().min(1),
|
|
17064
|
+
decision: external_exports.enum(["approve", "decline"]),
|
|
17065
|
+
externalAuthId: external_exports.string().min(1).optional(),
|
|
17066
|
+
email: external_exports.string().optional(),
|
|
17067
|
+
acceptTerms: external_exports.boolean().optional()
|
|
17068
|
+
}).strict();
|
|
17069
|
+
var deviceConfirmRecordedSchema = external_exports.object({
|
|
17070
|
+
status: external_exports.literal(DEVICE_AUTH_STATUS.RECORDED),
|
|
17071
|
+
accountId: external_exports.string().min(1).optional()
|
|
17072
|
+
}).strict();
|
|
17073
|
+
var deviceConfirmResponseSchema = external_exports.union([
|
|
17074
|
+
deviceConfirmRecordedSchema,
|
|
17075
|
+
deviceAuthRefusalSchema
|
|
17076
|
+
]);
|
|
17077
|
+
var DASHBOARD_URL = "https://app.halfcycle.ai";
|
|
17078
|
+
|
|
17079
|
+
// ../events/dist/project-page.js
|
|
17080
|
+
function projectPageUrl(engagementId) {
|
|
17081
|
+
return isEngagementId(engagementId) ? `${DASHBOARD_URL}/${encodeURIComponent(engagementId)}` : void 0;
|
|
17082
|
+
}
|
|
17083
|
+
function requestAccessSentence(engagementId) {
|
|
17084
|
+
const page = projectPageUrl(engagementId) ?? "this project's page on Halfcycle";
|
|
17085
|
+
return `To ask for access, open ${page} signed in as this account and choose "Request access". Then let the project's owner know, so they can approve it.`;
|
|
17086
|
+
}
|
|
17087
|
+
|
|
16089
17088
|
// ../events/dist/credential-wire.js
|
|
16090
17089
|
var engagementCredentialResponseSchema = external_exports.object({
|
|
16091
17090
|
engagementId: external_exports.string(),
|
|
@@ -16169,6 +17168,56 @@ var chatScopeResultSchema = external_exports.discriminatedUnion("outcome", [
|
|
|
16169
17168
|
bareOutcome("unavailable")
|
|
16170
17169
|
]);
|
|
16171
17170
|
|
|
17171
|
+
// ../events/dist/review-chat.js
|
|
17172
|
+
var chatReviewAcceptRequestSchema = external_exports.object({
|
|
17173
|
+
act: external_exports.literal("accept"),
|
|
17174
|
+
reference: phaseIdentitySchema,
|
|
17175
|
+
reviewVersion: external_exports.number().int().min(0),
|
|
17176
|
+
parts: external_exports.array(systemPartIdSchema).max(REVIEW_PARTS_MAX)
|
|
17177
|
+
}).strict();
|
|
17178
|
+
function notedRequest(act) {
|
|
17179
|
+
return external_exports.object({
|
|
17180
|
+
act: external_exports.literal(act),
|
|
17181
|
+
reference: phaseIdentitySchema,
|
|
17182
|
+
reviewVersion: external_exports.number().int().min(0),
|
|
17183
|
+
part: systemPartIdSchema,
|
|
17184
|
+
note: reviewNoteWireSchema
|
|
17185
|
+
}).strict();
|
|
17186
|
+
}
|
|
17187
|
+
var chatReviewQuestionRequestSchema = notedRequest("question");
|
|
17188
|
+
var chatReviewSendBackRequestSchema = notedRequest("send-back");
|
|
17189
|
+
var chatReviewCarryRequestSchema = notedRequest("carry");
|
|
17190
|
+
var chatReviewDropRequestSchema = notedRequest("drop");
|
|
17191
|
+
var chatReviewRequestSchema = external_exports.discriminatedUnion("act", [
|
|
17192
|
+
chatReviewAcceptRequestSchema,
|
|
17193
|
+
chatReviewQuestionRequestSchema,
|
|
17194
|
+
chatReviewSendBackRequestSchema,
|
|
17195
|
+
chatReviewCarryRequestSchema,
|
|
17196
|
+
chatReviewDropRequestSchema
|
|
17197
|
+
]);
|
|
17198
|
+
function bareOutcome2(outcome) {
|
|
17199
|
+
return external_exports.object({ outcome: external_exports.literal(outcome) }).strict();
|
|
17200
|
+
}
|
|
17201
|
+
var chatReviewResultSchema = external_exports.discriminatedUnion("outcome", [
|
|
17202
|
+
...reviewDecisionResultSchema.options,
|
|
17203
|
+
bareOutcome2("show-first"),
|
|
17204
|
+
bareOutcome2("not-offered")
|
|
17205
|
+
]);
|
|
17206
|
+
var reviewAnswerRequestSchema = external_exports.object({
|
|
17207
|
+
part: systemPartIdSchema,
|
|
17208
|
+
answer: statementText(REVIEW_ANSWER_MAX)
|
|
17209
|
+
}).strict();
|
|
17210
|
+
var reviewAnswerRecordedSchema = external_exports.object({
|
|
17211
|
+
outcome: external_exports.literal("recorded"),
|
|
17212
|
+
name: systemPartSchema.shape.name
|
|
17213
|
+
}).strict();
|
|
17214
|
+
var reviewAnswerResultSchema = external_exports.discriminatedUnion("outcome", [
|
|
17215
|
+
reviewAnswerRecordedSchema,
|
|
17216
|
+
bareOutcome2("no-question"),
|
|
17217
|
+
reviewRefusedSchema,
|
|
17218
|
+
bareOutcome2("unavailable")
|
|
17219
|
+
]);
|
|
17220
|
+
|
|
16172
17221
|
// dist/hook-protocol.js
|
|
16173
17222
|
import { writeSync } from "node:fs";
|
|
16174
17223
|
function emitBlock(reason) {
|
|
@@ -16224,12 +17273,10 @@ This is not a service outage: the guard service answered. The suspect is this re
|
|
|
16224
17273
|
}
|
|
16225
17274
|
var UNCONFIGURED_EXIT_TOOL_USE = 2;
|
|
16226
17275
|
var UNCONFIGURED_EXIT_STOP_FAMILY = 1;
|
|
16227
|
-
function emitNeverConfigured(exitCode, reason) {
|
|
16228
|
-
writeSync(2, neverConfiguredReport(reason));
|
|
17276
|
+
function emitNeverConfigured(exitCode, reason, engagementId) {
|
|
17277
|
+
writeSync(2, neverConfiguredReport(reason, void 0, engagementId));
|
|
16229
17278
|
process.exitCode = exitCode;
|
|
16230
17279
|
}
|
|
16231
|
-
var SIGN_IN_COMMAND = '"npx halfcycle"';
|
|
16232
|
-
var HOOK_REMEDY = `Run ${SIGN_IN_COMMAND} in the project folder to sign in.`;
|
|
16233
17280
|
function namesTheSignInCommand(text) {
|
|
16234
17281
|
return text.includes(SIGN_IN_COMMAND);
|
|
16235
17282
|
}
|
|
@@ -16255,16 +17302,16 @@ ${NOT_A_SERVICE_OUTAGE} Nothing was sent: this job holds no credential and was n
|
|
|
16255
17302
|
${CI_OLDER_SETUP}
|
|
16256
17303
|
`;
|
|
16257
17304
|
}
|
|
16258
|
-
function notThisAccountReport(label) {
|
|
16259
|
-
return `${label} This project belongs to another Halfcycle account. Halfcycle is Solo today \u2014 one person works on a project at a time \u2014 so no credential exists for this machine and none will be issued. Nothing here needs fixing; this copy simply is not checked.
|
|
17305
|
+
function notThisAccountReport(label, engagementId) {
|
|
17306
|
+
return `${label} This project belongs to another Halfcycle account. Halfcycle is Solo today \u2014 one person works on a project at a time \u2014 so no credential exists for this machine and none will be issued. Nothing here needs fixing; this copy simply is not checked. ${requestAccessSentence(engagementId)} If this account is wrong, sign in as the one that owns the project and run "npx halfcycle" again.
|
|
16260
17307
|
`;
|
|
16261
17308
|
}
|
|
16262
|
-
function neverConfiguredReport(reason, label = "[Halfcycle]") {
|
|
17309
|
+
function neverConfiguredReport(reason, label = "[Halfcycle]", engagementId) {
|
|
16263
17310
|
switch (reason) {
|
|
16264
17311
|
case "no-credential-yet":
|
|
16265
17312
|
return neverConfiguredMessage(HOOK_REMEDY, label);
|
|
16266
17313
|
case "not-this-account":
|
|
16267
|
-
return notThisAccountReport(label);
|
|
17314
|
+
return notThisAccountReport(label, engagementId);
|
|
16268
17315
|
}
|
|
16269
17316
|
}
|
|
16270
17317
|
var CREDENTIAL_REJECTED_STATUSES = /* @__PURE__ */ new Set([401, 403]);
|
|
@@ -16315,14 +17362,14 @@ function credentialRenewalFailedMessage(remedy, label = "[Halfcycle]") {
|
|
|
16315
17362
|
${remedy}
|
|
16316
17363
|
`;
|
|
16317
17364
|
}
|
|
16318
|
-
function noCoverageStatement(reason, remedySaid = false) {
|
|
17365
|
+
function noCoverageStatement(reason, remedySaid = false, engagementId) {
|
|
16319
17366
|
if (reason === "not-this-account") {
|
|
16320
17367
|
return [
|
|
16321
17368
|
"[halfcycle] This project belongs to another Halfcycle account, so nothing here is checked.",
|
|
16322
17369
|
"[halfcycle] Your work carries on exactly as it would without Halfcycle \u2014 no edit is",
|
|
16323
17370
|
"[halfcycle] evaluated, and nothing you do is reported as having passed.",
|
|
16324
17371
|
"[halfcycle] Halfcycle is Solo today \u2014 one developer per project \u2014 with Team plans coming.",
|
|
16325
|
-
|
|
17372
|
+
`[halfcycle] ${requestAccessSentence(engagementId)}`,
|
|
16326
17373
|
"[halfcycle] If this account is wrong, sign in as the one that owns the project and run",
|
|
16327
17374
|
'[halfcycle] "npx halfcycle" again.'
|
|
16328
17375
|
].join("\n");
|
|
@@ -16556,13 +17603,16 @@ import { spawnSync } from "node:child_process";
|
|
|
16556
17603
|
import { existsSync, readFileSync } from "node:fs";
|
|
16557
17604
|
import { relative } from "node:path";
|
|
16558
17605
|
var COLOR_AWARE_SUBCOMMANDS = /* @__PURE__ */ new Set(["diff", "show", "log"]);
|
|
16559
|
-
function
|
|
17606
|
+
function spawnGit(args2, cwd, timeoutMs) {
|
|
16560
17607
|
const argv = COLOR_AWARE_SUBCOMMANDS.has(args2[0] ?? "") ? [args2[0], "--no-color", ...args2.slice(1)] : args2;
|
|
16561
|
-
|
|
17608
|
+
return spawnSync("git", argv, {
|
|
16562
17609
|
cwd,
|
|
16563
17610
|
encoding: "utf-8",
|
|
16564
17611
|
...timeoutMs === void 0 ? {} : { timeout: timeoutMs }
|
|
16565
17612
|
});
|
|
17613
|
+
}
|
|
17614
|
+
function git(args2, cwd, timeoutMs) {
|
|
17615
|
+
const result = spawnGit(args2, cwd, timeoutMs);
|
|
16566
17616
|
if (result.error) {
|
|
16567
17617
|
const missing = result.error.code === "ENOENT" && existsSync(cwd);
|
|
16568
17618
|
return { ok: false, message: `git error: ${result.error.message}`, ...missing ? { gitMissing: true } : {} };
|
|
@@ -16724,6 +17774,124 @@ function getWiringStatus(pathspecs, cwd) {
|
|
|
16724
17774
|
...pathspecs
|
|
16725
17775
|
], cwd, WIRING_STATUS_TIMEOUT_MS);
|
|
16726
17776
|
}
|
|
17777
|
+
var OBSERVE_GIT_TIMEOUT_MS = 5e3;
|
|
17778
|
+
function isFullCommit(value) {
|
|
17779
|
+
return closeCommitSchema.safeParse(value).success;
|
|
17780
|
+
}
|
|
17781
|
+
function notACommit(name) {
|
|
17782
|
+
return { ok: false, message: `${name} is not a full commit id` };
|
|
17783
|
+
}
|
|
17784
|
+
function headCommit(repoRoot) {
|
|
17785
|
+
const result = git(["rev-parse", "--verify", "HEAD^{commit}"], repoRoot, OBSERVE_GIT_TIMEOUT_MS);
|
|
17786
|
+
if (!result.ok)
|
|
17787
|
+
return result;
|
|
17788
|
+
const commit = result.value.trim();
|
|
17789
|
+
return isFullCommit(commit) ? { ok: true, value: commit } : notACommit("HEAD");
|
|
17790
|
+
}
|
|
17791
|
+
function headCommittedAt(head, repoRoot) {
|
|
17792
|
+
if (!isFullCommit(head))
|
|
17793
|
+
return notACommit("head");
|
|
17794
|
+
const result = git(["log", "-1", "--format=%cI", head], repoRoot, OBSERVE_GIT_TIMEOUT_MS);
|
|
17795
|
+
if (!result.ok)
|
|
17796
|
+
return result;
|
|
17797
|
+
const when = new Date(result.value.trim());
|
|
17798
|
+
if (Number.isNaN(when.getTime()))
|
|
17799
|
+
return { ok: false, message: "git gave no readable commit time" };
|
|
17800
|
+
const iso = when.toISOString();
|
|
17801
|
+
return utcIso8601.safeParse(iso).success ? { ok: true, value: iso } : { ok: false, message: "git gave no readable commit time" };
|
|
17802
|
+
}
|
|
17803
|
+
function isAncestor(start, head, repoRoot) {
|
|
17804
|
+
if (!isFullCommit(start))
|
|
17805
|
+
return notACommit("start");
|
|
17806
|
+
if (!isFullCommit(head))
|
|
17807
|
+
return notACommit("head");
|
|
17808
|
+
const result = spawnGit(["merge-base", "--is-ancestor", start, head], repoRoot, OBSERVE_GIT_TIMEOUT_MS);
|
|
17809
|
+
if (result.error)
|
|
17810
|
+
return { ok: false, message: `git error: ${result.error.message}` };
|
|
17811
|
+
if (result.status === 0)
|
|
17812
|
+
return { ok: true, value: true };
|
|
17813
|
+
if (result.status === 1)
|
|
17814
|
+
return { ok: true, value: false };
|
|
17815
|
+
const stderr = result.stderr?.trim() ?? "";
|
|
17816
|
+
return { ok: false, message: `git merge-base failed: ${stderr || "non-zero exit"}` };
|
|
17817
|
+
}
|
|
17818
|
+
function commitCount(start, head, repoRoot) {
|
|
17819
|
+
if (!isFullCommit(start))
|
|
17820
|
+
return notACommit("start");
|
|
17821
|
+
if (!isFullCommit(head))
|
|
17822
|
+
return notACommit("head");
|
|
17823
|
+
const result = git(["rev-list", "--count", `${start}..${head}`], repoRoot, OBSERVE_GIT_TIMEOUT_MS);
|
|
17824
|
+
if (!result.ok)
|
|
17825
|
+
return result;
|
|
17826
|
+
const text = result.value.trim();
|
|
17827
|
+
if (!/^\d+$/.test(text))
|
|
17828
|
+
return { ok: false, message: "git gave no readable commit count" };
|
|
17829
|
+
return { ok: true, value: Number(text) };
|
|
17830
|
+
}
|
|
17831
|
+
function rangeNameStatus(start, head, repoRoot) {
|
|
17832
|
+
if (!isFullCommit(start))
|
|
17833
|
+
return notACommit("start");
|
|
17834
|
+
if (!isFullCommit(head))
|
|
17835
|
+
return notACommit("head");
|
|
17836
|
+
const result = git(["diff", "--name-status", "--no-renames", "-z", start, head], repoRoot, OBSERVE_GIT_TIMEOUT_MS);
|
|
17837
|
+
if (!result.ok)
|
|
17838
|
+
return result;
|
|
17839
|
+
return parseNameStatusZ(result.value);
|
|
17840
|
+
}
|
|
17841
|
+
function parseNameStatusZ(output) {
|
|
17842
|
+
if (output === "")
|
|
17843
|
+
return { ok: true, value: [] };
|
|
17844
|
+
if (!output.endsWith("\0"))
|
|
17845
|
+
return { ok: false, message: "git gave an unreadable list of changed files" };
|
|
17846
|
+
const fields = output.slice(0, -1).split("\0");
|
|
17847
|
+
if (fields.length % 2 !== 0)
|
|
17848
|
+
return { ok: false, message: "git gave an unreadable list of changed files" };
|
|
17849
|
+
const entries = [];
|
|
17850
|
+
for (let i = 0; i < fields.length; i += 2) {
|
|
17851
|
+
const status = fields[i];
|
|
17852
|
+
const path = fields[i + 1];
|
|
17853
|
+
if (!/^[A-Z][0-9]*$/.test(status) || status.startsWith("R") || status.startsWith("C") || path === "") {
|
|
17854
|
+
return { ok: false, message: "git gave an unreadable list of changed files" };
|
|
17855
|
+
}
|
|
17856
|
+
entries.push({ status: status.charAt(0), path });
|
|
17857
|
+
}
|
|
17858
|
+
return { ok: true, value: entries };
|
|
17859
|
+
}
|
|
17860
|
+
function readCodeLinkFacts(repoRoot, head) {
|
|
17861
|
+
const nothing = { repositoryAddress: null, headOnGitHub: false };
|
|
17862
|
+
if (!isFullCommit(head))
|
|
17863
|
+
return nothing;
|
|
17864
|
+
try {
|
|
17865
|
+
const remote = currentRemote(repoRoot);
|
|
17866
|
+
const url2 = git(["remote", "get-url", "--", remote], repoRoot, OBSERVE_GIT_TIMEOUT_MS);
|
|
17867
|
+
if (!url2.ok)
|
|
17868
|
+
return nothing;
|
|
17869
|
+
const repositoryAddress = githubRepositoryAddress(url2.value.trim());
|
|
17870
|
+
if (repositoryAddress === null)
|
|
17871
|
+
return nothing;
|
|
17872
|
+
const refs = git(["for-each-ref", "--contains", head, "--format=%(refname)", `refs/remotes/${remote}/`], repoRoot, OBSERVE_GIT_TIMEOUT_MS);
|
|
17873
|
+
if (!refs.ok)
|
|
17874
|
+
return nothing;
|
|
17875
|
+
const facts = { repositoryAddress, headOnGitHub: refs.value.trim() !== "" };
|
|
17876
|
+
return codeLinkFactsSchema.safeParse(facts).success ? facts : nothing;
|
|
17877
|
+
} catch {
|
|
17878
|
+
return nothing;
|
|
17879
|
+
}
|
|
17880
|
+
}
|
|
17881
|
+
function currentRemote(repoRoot) {
|
|
17882
|
+
const symbolic = git(["symbolic-ref", "--quiet", "HEAD"], repoRoot, OBSERVE_GIT_TIMEOUT_MS);
|
|
17883
|
+
if (!symbolic.ok)
|
|
17884
|
+
return "origin";
|
|
17885
|
+
const ref = symbolic.value.trim();
|
|
17886
|
+
if (!ref.startsWith("refs/heads/"))
|
|
17887
|
+
return "origin";
|
|
17888
|
+
const branch = ref.slice("refs/heads/".length);
|
|
17889
|
+
const configured = git(["config", "--get", `branch.${branch}.remote`], repoRoot, OBSERVE_GIT_TIMEOUT_MS);
|
|
17890
|
+
if (!configured.ok)
|
|
17891
|
+
return "origin";
|
|
17892
|
+
const remote = configured.value.trim();
|
|
17893
|
+
return remote === "" || remote === "." ? "origin" : remote;
|
|
17894
|
+
}
|
|
16727
17895
|
|
|
16728
17896
|
// dist/screen/secret-shape.js
|
|
16729
17897
|
function screenExcerpts(excerpts) {
|
|
@@ -18108,8 +19276,36 @@ function cleanRunSummary(verdict, scope) {
|
|
|
18108
19276
|
return `[Halfcycle CI] ${verdict} ${files}, ${scope.signals} signal(s) checked.`;
|
|
18109
19277
|
}
|
|
18110
19278
|
|
|
19279
|
+
// dist/pinned-engagement.js
|
|
19280
|
+
import { readFileSync as readFileSync5 } from "node:fs";
|
|
19281
|
+
import { resolve } from "node:path";
|
|
19282
|
+
function readPinnedId(root) {
|
|
19283
|
+
let raw;
|
|
19284
|
+
try {
|
|
19285
|
+
raw = readFileSync5(resolve(root, BUNDLE_PIN_REL), "utf-8");
|
|
19286
|
+
} catch {
|
|
19287
|
+
return { problem: "no-pin" };
|
|
19288
|
+
}
|
|
19289
|
+
let pin;
|
|
19290
|
+
try {
|
|
19291
|
+
pin = JSON.parse(raw);
|
|
19292
|
+
} catch {
|
|
19293
|
+
return { problem: "no-id" };
|
|
19294
|
+
}
|
|
19295
|
+
const id = typeof pin === "object" && pin !== null ? pin[PIN_ENGAGEMENT_ID_FIELD] : void 0;
|
|
19296
|
+
return typeof id === "string" && id !== "" ? { id } : { problem: "no-id" };
|
|
19297
|
+
}
|
|
19298
|
+
function pinnedEngagementId(root) {
|
|
19299
|
+
const pinned = readPinnedId(root);
|
|
19300
|
+
return "id" in pinned && isEngagementId(pinned.id) ? pinned.id : void 0;
|
|
19301
|
+
}
|
|
19302
|
+
function projectFolder(env = process.env) {
|
|
19303
|
+
const dir = env["CLAUDE_PROJECT_DIR"];
|
|
19304
|
+
return dir !== void 0 && dir !== "" ? dir : process.cwd();
|
|
19305
|
+
}
|
|
19306
|
+
|
|
18111
19307
|
// dist/credential-store.js
|
|
18112
|
-
import { chmodSync, existsSync as existsSync3, readFileSync as
|
|
19308
|
+
import { chmodSync, existsSync as existsSync3, readFileSync as readFileSync6, renameSync as renameSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2, realpathSync } from "node:fs";
|
|
18113
19309
|
import { homedir as homedir2, platform } from "node:os";
|
|
18114
19310
|
import { dirname as dirname3, join as join5 } from "node:path";
|
|
18115
19311
|
var REFRESHED_TOKEN_KEYS = ["HALFCYCLE_TOKEN", "GUARD_SERVICE_TOKEN"];
|
|
@@ -18123,7 +19319,7 @@ function hasNotThisAccountMarker(engagementId, home) {
|
|
|
18123
19319
|
function readAccountCredential(controlOrigin, home) {
|
|
18124
19320
|
let raw;
|
|
18125
19321
|
try {
|
|
18126
|
-
raw =
|
|
19322
|
+
raw = readFileSync6(join5(home ?? homedir2(), HALFCYCLE_DIR_NAME, ACCOUNT_STORE_FILENAME), "utf-8");
|
|
18127
19323
|
} catch {
|
|
18128
19324
|
return void 0;
|
|
18129
19325
|
}
|
|
@@ -18140,7 +19336,7 @@ function readStoredEngagementToken(envFilePath) {
|
|
|
18140
19336
|
function readEngagementStore(envFilePath) {
|
|
18141
19337
|
let raw;
|
|
18142
19338
|
try {
|
|
18143
|
-
raw =
|
|
19339
|
+
raw = readFileSync6(envFilePath, "utf-8");
|
|
18144
19340
|
} catch {
|
|
18145
19341
|
return void 0;
|
|
18146
19342
|
}
|
|
@@ -18170,7 +19366,7 @@ function writeRefreshedToken(opts) {
|
|
|
18170
19366
|
}
|
|
18171
19367
|
let existing;
|
|
18172
19368
|
try {
|
|
18173
|
-
existing =
|
|
19369
|
+
existing = readFileSync6(declaredReal, "utf-8");
|
|
18174
19370
|
} catch {
|
|
18175
19371
|
existing = null;
|
|
18176
19372
|
}
|
|
@@ -18313,7 +19509,7 @@ async function evaluateWithRefresh(opts) {
|
|
|
18313
19509
|
|
|
18314
19510
|
// dist/hooks/marker.js
|
|
18315
19511
|
import { createHash } from "node:crypto";
|
|
18316
|
-
import { existsSync as existsSync4, readFileSync as
|
|
19512
|
+
import { existsSync as existsSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync3, unlinkSync as unlinkSync3, readdirSync as readdirSync3, statSync } from "node:fs";
|
|
18317
19513
|
import { tmpdir } from "node:os";
|
|
18318
19514
|
import { join as join6 } from "node:path";
|
|
18319
19515
|
var MARKER_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
@@ -18345,7 +19541,7 @@ function readMarker(repoRoot, sessionId) {
|
|
|
18345
19541
|
if (!existsSync4(path))
|
|
18346
19542
|
return null;
|
|
18347
19543
|
try {
|
|
18348
|
-
return
|
|
19544
|
+
return readFileSync7(path, "utf-8").trim() || null;
|
|
18349
19545
|
} catch {
|
|
18350
19546
|
return null;
|
|
18351
19547
|
}
|
|
@@ -18363,7 +19559,10 @@ var SESSION_NOTICE_SUFFIX = {
|
|
|
18363
19559
|
"no-repository": ".norepo",
|
|
18364
19560
|
"credential-rejected": ".credential",
|
|
18365
19561
|
"credential-renewed-for-this-run": ".renewed",
|
|
18366
|
-
"credential-missing": ".nocredential"
|
|
19562
|
+
"credential-missing": ".nocredential",
|
|
19563
|
+
"observe-no-start-here": ".observe-nostart",
|
|
19564
|
+
"observe-not-on-branch": ".observe-nobranch",
|
|
19565
|
+
"observe-no-start-recorded": ".observe-norecord"
|
|
18367
19566
|
};
|
|
18368
19567
|
function firstNoticeThisSession(notice, folder, sessionId) {
|
|
18369
19568
|
if (!sessionId)
|
|
@@ -18391,7 +19590,7 @@ function readDedupe(repoRoot, sessionId) {
|
|
|
18391
19590
|
if (!existsSync4(path))
|
|
18392
19591
|
return null;
|
|
18393
19592
|
try {
|
|
18394
|
-
const parsed = JSON.parse(
|
|
19593
|
+
const parsed = JSON.parse(readFileSync7(path, "utf-8"));
|
|
18395
19594
|
if (typeof parsed === "object" && parsed !== null && typeof parsed.base === "string" && typeof parsed.digest === "string") {
|
|
18396
19595
|
return parsed;
|
|
18397
19596
|
}
|
|
@@ -18436,7 +19635,7 @@ function orphanSweep(repoRoot, refKnown, keepSessionId, now = Date.now()) {
|
|
|
18436
19635
|
const full = join6(dir, name);
|
|
18437
19636
|
let unusable = false;
|
|
18438
19637
|
try {
|
|
18439
|
-
const ref =
|
|
19638
|
+
const ref = readFileSync7(full, "utf-8").trim();
|
|
18440
19639
|
if (!ref || !refKnown(ref))
|
|
18441
19640
|
unusable = true;
|
|
18442
19641
|
} catch {
|
|
@@ -18458,11 +19657,19 @@ function orphanSweep(repoRoot, refKnown, keepSessionId, now = Date.now()) {
|
|
|
18458
19657
|
}
|
|
18459
19658
|
}
|
|
18460
19659
|
}
|
|
19660
|
+
function observeThrottlePath(folder) {
|
|
19661
|
+
return join6(markerDir(), `${MARKER_PREFIX}${repoHash(folder)}.observe-throttle`);
|
|
19662
|
+
}
|
|
19663
|
+
function observeNoticesPath(folder, sessionId) {
|
|
19664
|
+
if (!sessionId)
|
|
19665
|
+
return null;
|
|
19666
|
+
return join6(markerDir(), `${MARKER_PREFIX}${repoHash(folder)}-${sessionId}.observe-said`);
|
|
19667
|
+
}
|
|
18461
19668
|
|
|
18462
19669
|
// dist/seat-configuration.js
|
|
18463
|
-
import { existsSync as existsSync5, readFileSync as
|
|
19670
|
+
import { existsSync as existsSync5, readFileSync as readFileSync8, realpathSync as realpathSync2 } from "node:fs";
|
|
18464
19671
|
import { homedir as homedir3 } from "node:os";
|
|
18465
|
-
import { join as join7, relative as relative3, resolve } from "node:path";
|
|
19672
|
+
import { join as join7, relative as relative3, resolve as resolve2 } from "node:path";
|
|
18466
19673
|
function gatherSeatConfiguration(repoRoot, read = readSeatConfiguration) {
|
|
18467
19674
|
try {
|
|
18468
19675
|
const facts = read(repoRoot);
|
|
@@ -18475,7 +19682,7 @@ function gatherSeatConfiguration(repoRoot, read = readSeatConfiguration) {
|
|
|
18475
19682
|
}
|
|
18476
19683
|
}
|
|
18477
19684
|
function readSeatConfiguration(repoRoot) {
|
|
18478
|
-
const given =
|
|
19685
|
+
const given = resolve2(process.env["CLAUDE_PROJECT_DIR"] || process.cwd());
|
|
18479
19686
|
const projectDir = realpathSync2.native(given);
|
|
18480
19687
|
const home = homedir3();
|
|
18481
19688
|
let realHome = home;
|
|
@@ -18497,7 +19704,7 @@ function isPlainObject3(value) {
|
|
|
18497
19704
|
function readSettingsFile(projectDir, rel) {
|
|
18498
19705
|
let raw;
|
|
18499
19706
|
try {
|
|
18500
|
-
raw =
|
|
19707
|
+
raw = readFileSync8(join7(projectDir, rel), "utf-8");
|
|
18501
19708
|
} catch (err) {
|
|
18502
19709
|
const code = err.code;
|
|
18503
19710
|
const state = code === "ENOENT" || code === "ENOTDIR" ? "absent" : "unreadable";
|
|
@@ -18655,7 +19862,7 @@ async function evaluateEdit(reply, readSeat) {
|
|
|
18655
19862
|
await emitRunRecord({ runType: "hook", outcome: "unconfigured", changeSet });
|
|
18656
19863
|
if (!firstNoticeThisSession("credential-missing", repoRoot, input.session_id))
|
|
18657
19864
|
return;
|
|
18658
|
-
emitNeverConfigured(UNCONFIGURED_EXIT_TOOL_USE, readNeverConfiguredReason());
|
|
19865
|
+
emitNeverConfigured(UNCONFIGURED_EXIT_TOOL_USE, readNeverConfiguredReason(), pinnedEngagementId(projectFolder()));
|
|
18659
19866
|
return;
|
|
18660
19867
|
}
|
|
18661
19868
|
const { guardServiceUrl, guardServiceToken, guardEngagementId } = configResult.config;
|
|
@@ -18748,10 +19955,10 @@ ${guardLines}${notRunBlock}`);
|
|
|
18748
19955
|
}
|
|
18749
19956
|
}
|
|
18750
19957
|
function readStdin() {
|
|
18751
|
-
return new Promise((
|
|
19958
|
+
return new Promise((resolve5, reject) => {
|
|
18752
19959
|
const chunks = [];
|
|
18753
19960
|
process.stdin.on("data", (chunk) => chunks.push(chunk));
|
|
18754
|
-
process.stdin.on("end", () =>
|
|
19961
|
+
process.stdin.on("end", () => resolve5(Buffer.concat(chunks).toString("utf-8")));
|
|
18755
19962
|
process.stdin.on("error", reject);
|
|
18756
19963
|
});
|
|
18757
19964
|
}
|
|
@@ -18791,9 +19998,11 @@ function collectDiff(repoRoot, base) {
|
|
|
18791
19998
|
getDiff: (relPath) => getWorkingTreeFileDiff(relPath, repoRoot)
|
|
18792
19999
|
};
|
|
18793
20000
|
}
|
|
18794
|
-
async function runSessionDiff(input, kind, readSeat = readSeatConfiguration) {
|
|
20001
|
+
async function runSessionDiff(input, kind, readSeat = readSeatConfiguration, afterPass) {
|
|
18795
20002
|
const reply = hookReply(STOP_HOOK_EVENT[kind]);
|
|
18796
20003
|
await evaluateSession(input, kind, reply, readSeat);
|
|
20004
|
+
if (afterPass)
|
|
20005
|
+
await afterPass(reply);
|
|
18797
20006
|
reply.flush();
|
|
18798
20007
|
}
|
|
18799
20008
|
async function evaluateSession(input, kind, reply, readSeat) {
|
|
@@ -18836,7 +20045,7 @@ async function evaluateSession(input, kind, reply, readSeat) {
|
|
|
18836
20045
|
await emitRunRecord({ runType: "hook", outcome: "unconfigured", changeSet, diffBase });
|
|
18837
20046
|
if (!firstNoticeThisSession("credential-missing", repoRoot, sessionId))
|
|
18838
20047
|
return;
|
|
18839
|
-
emitNeverConfigured(UNCONFIGURED_EXIT_STOP_FAMILY, readNeverConfiguredReason());
|
|
20048
|
+
emitNeverConfigured(UNCONFIGURED_EXIT_STOP_FAMILY, readNeverConfiguredReason(), pinnedEngagementId(projectFolder()));
|
|
18840
20049
|
return;
|
|
18841
20050
|
}
|
|
18842
20051
|
const { guardServiceUrl, guardServiceToken, guardEngagementId } = configResult.config;
|
|
@@ -18947,9 +20156,9 @@ async function readHookStdin() {
|
|
|
18947
20156
|
}
|
|
18948
20157
|
}
|
|
18949
20158
|
function readStdin2() {
|
|
18950
|
-
return new Promise((
|
|
20159
|
+
return new Promise((resolve5) => {
|
|
18951
20160
|
if (process.stdin.isTTY) {
|
|
18952
|
-
|
|
20161
|
+
resolve5("");
|
|
18953
20162
|
return;
|
|
18954
20163
|
}
|
|
18955
20164
|
const chunks = [];
|
|
@@ -18959,7 +20168,7 @@ function readStdin2() {
|
|
|
18959
20168
|
return;
|
|
18960
20169
|
settled = true;
|
|
18961
20170
|
clearTimeout(timer);
|
|
18962
|
-
|
|
20171
|
+
resolve5(Buffer.concat(chunks).toString("utf-8"));
|
|
18963
20172
|
};
|
|
18964
20173
|
const timer = setTimeout(done, HOOK_STDIN_GRACE_MS);
|
|
18965
20174
|
process.stdin.on("data", (chunk) => chunks.push(chunk));
|
|
@@ -18974,17 +20183,328 @@ async function runSubagentStop() {
|
|
|
18974
20183
|
await runSessionDiff(input, "subagent-stop");
|
|
18975
20184
|
}
|
|
18976
20185
|
|
|
20186
|
+
// dist/observed.js
|
|
20187
|
+
import { readFileSync as readFileSync9, writeFileSync as writeFileSync4 } from "node:fs";
|
|
20188
|
+
function emptyTally() {
|
|
20189
|
+
return { added: 0, changed: 0, removed: 0, testFilesAdded: 0 };
|
|
20190
|
+
}
|
|
20191
|
+
function kindOf(status) {
|
|
20192
|
+
if (status === "A")
|
|
20193
|
+
return "added";
|
|
20194
|
+
if (status === "D")
|
|
20195
|
+
return "removed";
|
|
20196
|
+
return "changed";
|
|
20197
|
+
}
|
|
20198
|
+
function countObservedChanges(entries, description) {
|
|
20199
|
+
const methodFiles = { added: 0, changed: 0, removed: 0 };
|
|
20200
|
+
const byPart = /* @__PURE__ */ new Map();
|
|
20201
|
+
const outside = emptyTally();
|
|
20202
|
+
const paths = [];
|
|
20203
|
+
const listed = /* @__PURE__ */ new Set();
|
|
20204
|
+
for (const entry of entries) {
|
|
20205
|
+
const kind = kindOf(entry.status);
|
|
20206
|
+
if (isMethodFile(entry.path)) {
|
|
20207
|
+
methodFiles[kind] += 1;
|
|
20208
|
+
continue;
|
|
20209
|
+
}
|
|
20210
|
+
const owner = ownerOf(entry.path, description);
|
|
20211
|
+
let tally;
|
|
20212
|
+
if (owner === null) {
|
|
20213
|
+
tally = outside;
|
|
20214
|
+
if (entry.path.length >= 1 && entry.path.length <= OBSERVED_PATH_MAX && paths.length < OBSERVED_PATHS_LISTED_MAX && !listed.has(entry.path)) {
|
|
20215
|
+
paths.push(entry.path);
|
|
20216
|
+
listed.add(entry.path);
|
|
20217
|
+
}
|
|
20218
|
+
} else {
|
|
20219
|
+
tally = byPart.get(owner) ?? emptyTally();
|
|
20220
|
+
byPart.set(owner, tally);
|
|
20221
|
+
}
|
|
20222
|
+
tally[kind] += 1;
|
|
20223
|
+
if (kind === "added" && isTestFile(entry.path))
|
|
20224
|
+
tally.testFilesAdded += 1;
|
|
20225
|
+
}
|
|
20226
|
+
const parts = description.parts.filter((part) => byPart.has(part.id)).map((part) => ({ part: part.id, ...byPart.get(part.id) }));
|
|
20227
|
+
return { parts, methodFiles, outsideEveryPart: { ...outside, paths } };
|
|
20228
|
+
}
|
|
20229
|
+
var OBSERVE_MIN_INTERVAL_MS = 6e4;
|
|
20230
|
+
var OBSERVE_RECHECK_MS = 6e5;
|
|
20231
|
+
var OBSERVE_REQUEST_DEADLINE_MS = 1500;
|
|
20232
|
+
async function requestJson(fetchFn, url2, init) {
|
|
20233
|
+
const controller = new AbortController();
|
|
20234
|
+
const timer = setTimeout(() => controller.abort(), OBSERVE_REQUEST_DEADLINE_MS);
|
|
20235
|
+
try {
|
|
20236
|
+
const response = await fetchFn(url2, { ...init, signal: controller.signal });
|
|
20237
|
+
let body = null;
|
|
20238
|
+
try {
|
|
20239
|
+
body = await response.json();
|
|
20240
|
+
} catch {
|
|
20241
|
+
body = null;
|
|
20242
|
+
}
|
|
20243
|
+
if (controller.signal.aborted)
|
|
20244
|
+
return { kind: "unreachable" };
|
|
20245
|
+
return { kind: "answer", status: response.status, body };
|
|
20246
|
+
} catch {
|
|
20247
|
+
return { kind: "unreachable" };
|
|
20248
|
+
} finally {
|
|
20249
|
+
clearTimeout(timer);
|
|
20250
|
+
}
|
|
20251
|
+
}
|
|
20252
|
+
function refusalReason(status) {
|
|
20253
|
+
return status >= 500 ? "unreachable" : "refused";
|
|
20254
|
+
}
|
|
20255
|
+
async function attemptObservation(opts) {
|
|
20256
|
+
const { repoRoot, head, config: config2, controlOrigin, fetchFn } = opts;
|
|
20257
|
+
const base = `${controlOrigin}/engagements/${encodeURIComponent(config2.guardEngagementId)}/observed`;
|
|
20258
|
+
const authorization = `Bearer ${config2.guardServiceToken}`;
|
|
20259
|
+
const basisAnswer = await requestJson(fetchFn, `${base}/basis`, {
|
|
20260
|
+
method: "GET",
|
|
20261
|
+
headers: { Authorization: authorization }
|
|
20262
|
+
});
|
|
20263
|
+
if (basisAnswer.kind === "unreachable")
|
|
20264
|
+
return { outcome: "not-sent", reason: "unreachable" };
|
|
20265
|
+
if (basisAnswer.status < 200 || basisAnswer.status >= 300) {
|
|
20266
|
+
return { outcome: "not-sent", reason: refusalReason(basisAnswer.status) };
|
|
20267
|
+
}
|
|
20268
|
+
const parsedBasis = observedBasisSchema.safeParse(basisAnswer.body);
|
|
20269
|
+
if (!parsedBasis.success)
|
|
20270
|
+
return { outcome: "not-sent", reason: "refused" };
|
|
20271
|
+
const basis = parsedBasis.data;
|
|
20272
|
+
if (basis.outcome === "nothing-to-count")
|
|
20273
|
+
return { outcome: "not-sent", reason: "nothing-to-count" };
|
|
20274
|
+
if (basis.outcome === "cannot-count")
|
|
20275
|
+
return { outcome: "not-sent", reason: "cannot-count", phase: basis.phase };
|
|
20276
|
+
if (repoRoot === null || !refExists(basis.startCommit, repoRoot)) {
|
|
20277
|
+
return { outcome: "not-sent", reason: "start-not-here" };
|
|
20278
|
+
}
|
|
20279
|
+
if (head === null)
|
|
20280
|
+
return { outcome: "not-sent", reason: "start-not-on-branch" };
|
|
20281
|
+
const ancestor = isAncestor(basis.startCommit, head, repoRoot);
|
|
20282
|
+
if (!ancestor.ok)
|
|
20283
|
+
return { outcome: "not-sent", reason: "unreachable" };
|
|
20284
|
+
if (!ancestor.value)
|
|
20285
|
+
return { outcome: "not-sent", reason: "start-not-on-branch" };
|
|
20286
|
+
const commits = commitCount(basis.startCommit, head, repoRoot);
|
|
20287
|
+
const committedAt = headCommittedAt(head, repoRoot);
|
|
20288
|
+
const entries = rangeNameStatus(basis.startCommit, head, repoRoot);
|
|
20289
|
+
if (!commits.ok || !committedAt.ok || !entries.ok)
|
|
20290
|
+
return { outcome: "not-sent", reason: "unreachable" };
|
|
20291
|
+
const report = {
|
|
20292
|
+
phase: basis.phase,
|
|
20293
|
+
startCommit: basis.startCommit,
|
|
20294
|
+
headCommit: head,
|
|
20295
|
+
headCommittedAt: committedAt.value,
|
|
20296
|
+
commits: commits.value,
|
|
20297
|
+
descriptionVersion: basis.descriptionVersion,
|
|
20298
|
+
...countObservedChanges(entries.value, basis.description),
|
|
20299
|
+
codeLink: readCodeLinkFacts(repoRoot, head)
|
|
20300
|
+
};
|
|
20301
|
+
if (!observedChangesReportSchema.safeParse(report).success)
|
|
20302
|
+
return { outcome: "not-sent", reason: "refused" };
|
|
20303
|
+
const sent = await requestJson(fetchFn, base, {
|
|
20304
|
+
method: "POST",
|
|
20305
|
+
headers: { Authorization: authorization, "Content-Type": "application/json" },
|
|
20306
|
+
body: JSON.stringify(report)
|
|
20307
|
+
});
|
|
20308
|
+
if (sent.kind === "unreachable")
|
|
20309
|
+
return { outcome: "not-sent", reason: "unreachable" };
|
|
20310
|
+
if (sent.status === 409)
|
|
20311
|
+
return { outcome: "not-sent", reason: "basis-moved" };
|
|
20312
|
+
if (sent.status < 200 || sent.status >= 300)
|
|
20313
|
+
return { outcome: "not-sent", reason: refusalReason(sent.status) };
|
|
20314
|
+
const answer = observedChangesReportResultSchema.safeParse(sent.body);
|
|
20315
|
+
if (!answer.success)
|
|
20316
|
+
return { outcome: "not-sent", reason: "refused" };
|
|
20317
|
+
return answer.data.outcome === "kept" ? { outcome: "kept" } : { outcome: "recorded", answer: answer.data };
|
|
20318
|
+
}
|
|
20319
|
+
function readThrottle(folder) {
|
|
20320
|
+
try {
|
|
20321
|
+
const parsed = JSON.parse(readFileSync9(observeThrottlePath(folder), "utf-8"));
|
|
20322
|
+
if (typeof parsed !== "object" || parsed === null)
|
|
20323
|
+
return null;
|
|
20324
|
+
const { attemptedAt, accepted } = parsed;
|
|
20325
|
+
if (typeof attemptedAt !== "number")
|
|
20326
|
+
return null;
|
|
20327
|
+
const sent = accepted;
|
|
20328
|
+
const usable = sent !== null && sent !== void 0 && typeof sent.head === "string" && typeof sent.at === "number";
|
|
20329
|
+
return { attemptedAt, accepted: usable ? { head: sent.head, at: sent.at } : null };
|
|
20330
|
+
} catch {
|
|
20331
|
+
return null;
|
|
20332
|
+
}
|
|
20333
|
+
}
|
|
20334
|
+
function writeThrottle(folder, state) {
|
|
20335
|
+
try {
|
|
20336
|
+
writeFileSync4(observeThrottlePath(folder), JSON.stringify(state), "utf-8");
|
|
20337
|
+
} catch {
|
|
20338
|
+
}
|
|
20339
|
+
}
|
|
20340
|
+
function throttleAllows(state, head, now) {
|
|
20341
|
+
if (state === null)
|
|
20342
|
+
return true;
|
|
20343
|
+
if (now - state.attemptedAt < OBSERVE_MIN_INTERVAL_MS)
|
|
20344
|
+
return false;
|
|
20345
|
+
if (state.accepted === null)
|
|
20346
|
+
return true;
|
|
20347
|
+
if (head === null || head !== state.accepted.head)
|
|
20348
|
+
return true;
|
|
20349
|
+
return now - state.accepted.at >= OBSERVE_RECHECK_MS;
|
|
20350
|
+
}
|
|
20351
|
+
function locate(cwd) {
|
|
20352
|
+
const root = getRepoRoot(cwd);
|
|
20353
|
+
const repoRoot = root.ok ? root.value : null;
|
|
20354
|
+
const read = repoRoot === null ? null : headCommit(repoRoot);
|
|
20355
|
+
return { repoRoot, folder: repoRoot ?? cwd, head: read !== null && read.ok ? read.value : null };
|
|
20356
|
+
}
|
|
20357
|
+
function recordAttempt(folder, prior, head, at, attempt2) {
|
|
20358
|
+
const accepted = (attempt2.outcome === "recorded" || attempt2.outcome === "kept") && head !== null ? { head, at } : prior?.accepted ?? null;
|
|
20359
|
+
writeThrottle(folder, { attemptedAt: at, accepted });
|
|
20360
|
+
}
|
|
20361
|
+
async function runObserve(deps = {}) {
|
|
20362
|
+
let result;
|
|
20363
|
+
try {
|
|
20364
|
+
const configured = readConfig();
|
|
20365
|
+
if (!configured.config) {
|
|
20366
|
+
result = { outcome: "not-sent", reason: "not-configured" };
|
|
20367
|
+
} else {
|
|
20368
|
+
const now = deps.now ?? Date.now;
|
|
20369
|
+
const where = locate(deps.cwd ?? process.cwd());
|
|
20370
|
+
const prior = readThrottle(where.folder);
|
|
20371
|
+
const attempt2 = await attemptObservation({
|
|
20372
|
+
repoRoot: where.repoRoot,
|
|
20373
|
+
head: where.head,
|
|
20374
|
+
config: configured.config,
|
|
20375
|
+
controlOrigin: readControlOrigin().origin,
|
|
20376
|
+
fetchFn: deps.fetchFn ?? globalThis.fetch
|
|
20377
|
+
});
|
|
20378
|
+
recordAttempt(where.folder, prior, where.head, now(), attempt2);
|
|
20379
|
+
result = attempt2.outcome === "not-sent" ? { outcome: "not-sent", reason: attempt2.reason } : { outcome: attempt2.outcome };
|
|
20380
|
+
}
|
|
20381
|
+
} catch {
|
|
20382
|
+
result = { outcome: "not-sent", reason: "unreachable" };
|
|
20383
|
+
}
|
|
20384
|
+
return observeRunResultSchema.parse(result);
|
|
20385
|
+
}
|
|
20386
|
+
async function observeChanges(sessionId, reply, deps = {}) {
|
|
20387
|
+
try {
|
|
20388
|
+
const code = process.exitCode;
|
|
20389
|
+
if (code !== void 0 && code !== null && Number(code) !== 0)
|
|
20390
|
+
return;
|
|
20391
|
+
const configured = readConfig();
|
|
20392
|
+
if (!configured.config)
|
|
20393
|
+
return;
|
|
20394
|
+
const now = (deps.now ?? Date.now)();
|
|
20395
|
+
const where = locate(deps.cwd ?? process.cwd());
|
|
20396
|
+
const prior = readThrottle(where.folder);
|
|
20397
|
+
if (!throttleAllows(prior, where.head, now))
|
|
20398
|
+
return;
|
|
20399
|
+
writeThrottle(where.folder, { attemptedAt: now, accepted: prior?.accepted ?? null });
|
|
20400
|
+
const attempt2 = await attemptObservation({
|
|
20401
|
+
repoRoot: where.repoRoot,
|
|
20402
|
+
head: where.head,
|
|
20403
|
+
config: configured.config,
|
|
20404
|
+
controlOrigin: readControlOrigin().origin,
|
|
20405
|
+
fetchFn: deps.fetchFn ?? globalThis.fetch
|
|
20406
|
+
});
|
|
20407
|
+
recordAttempt(where.folder, prior, where.head, now, attempt2);
|
|
20408
|
+
for (const line2 of noticesFor(attempt2, where.folder, sessionId))
|
|
20409
|
+
reply.notice(line2);
|
|
20410
|
+
} catch {
|
|
20411
|
+
}
|
|
20412
|
+
}
|
|
20413
|
+
var R1_START_NOT_HERE = "Halfcycle cannot count what this phase has changed on this computer yet, because the commit the phase started from is not here. Get the project's latest history, and it counts again at the end of the next reply.";
|
|
20414
|
+
var R2_START_NOT_ON_BRANCH = "Halfcycle cannot count what this phase has changed on this branch, because the branch does not contain the commit the phase started from. Switch to the branch the phase is built on, and it counts again at the end of the next reply.";
|
|
20415
|
+
function r3NoStartRecorded(phase) {
|
|
20416
|
+
return `Halfcycle cannot count what phase ${phase} changes, because no starting commit was recorded when it opened. To count from where the project is now, run: ${HALFCYCLE_COMMAND} open-phase ${quoteForShell(phase)} --actor "you" --evidence "<what shows this phase is ready to start>"`;
|
|
20417
|
+
}
|
|
20418
|
+
function r4OutsideThePlan(parts) {
|
|
20419
|
+
const first = parts.length === 1 ? "Halfcycle saw changes to a part this phase's plan does not include:" : "Halfcycle saw changes to parts this phase's plan does not include:";
|
|
20420
|
+
const lines = parts.map((entry) => {
|
|
20421
|
+
const said = entry.said.length === 0 ? NO_TASK_SAID_LINE : entry.said.map((task) => agentSaid(task)).join("; ");
|
|
20422
|
+
return `"${entry.name}": ${said}`;
|
|
20423
|
+
});
|
|
20424
|
+
return [first, ...lines, "The project's owner sees them again at the review."].join("\n");
|
|
20425
|
+
}
|
|
20426
|
+
function r5OutsideEveryPart(files, example) {
|
|
20427
|
+
if (files === 1) {
|
|
20428
|
+
return `Halfcycle saw 1 changed file that no part of the system owns: ${example}. If it belongs to a part, add it to that part in the description of the system.`;
|
|
20429
|
+
}
|
|
20430
|
+
return `Halfcycle saw ${files} changed files that no part of the system owns, such as ${example}. If they belong to a part, add them to that part in the description of the system.`;
|
|
20431
|
+
}
|
|
20432
|
+
function readSaid(path) {
|
|
20433
|
+
const empty = { parts: {}, outsideFiles: 0 };
|
|
20434
|
+
if (path === null)
|
|
20435
|
+
return empty;
|
|
20436
|
+
try {
|
|
20437
|
+
const parsed = JSON.parse(readFileSync9(path, "utf-8"));
|
|
20438
|
+
const parts = {};
|
|
20439
|
+
if (typeof parsed.parts === "object" && parsed.parts !== null) {
|
|
20440
|
+
for (const [part, reasons] of Object.entries(parsed.parts)) {
|
|
20441
|
+
if (Array.isArray(reasons))
|
|
20442
|
+
parts[part] = reasons.filter((r) => typeof r === "string");
|
|
20443
|
+
}
|
|
20444
|
+
}
|
|
20445
|
+
return { parts, outsideFiles: typeof parsed.outsideFiles === "number" ? parsed.outsideFiles : 0 };
|
|
20446
|
+
} catch {
|
|
20447
|
+
return empty;
|
|
20448
|
+
}
|
|
20449
|
+
}
|
|
20450
|
+
function writeSaid(path, state) {
|
|
20451
|
+
if (path === null)
|
|
20452
|
+
return;
|
|
20453
|
+
try {
|
|
20454
|
+
writeFileSync4(path, JSON.stringify(state), "utf-8");
|
|
20455
|
+
} catch {
|
|
20456
|
+
}
|
|
20457
|
+
}
|
|
20458
|
+
function reasonsOf(entry) {
|
|
20459
|
+
return entry.said.filter((task) => task.whyOutsidePlan !== void 0).map((task) => `${task.title}\0${task.whyOutsidePlan}`);
|
|
20460
|
+
}
|
|
20461
|
+
function noticesFor(attempt2, folder, sessionId) {
|
|
20462
|
+
if (attempt2.outcome === "not-sent") {
|
|
20463
|
+
if (attempt2.reason === "start-not-here") {
|
|
20464
|
+
return firstNoticeThisSession("observe-no-start-here", folder, sessionId) ? [R1_START_NOT_HERE] : [];
|
|
20465
|
+
}
|
|
20466
|
+
if (attempt2.reason === "start-not-on-branch") {
|
|
20467
|
+
return firstNoticeThisSession("observe-not-on-branch", folder, sessionId) ? [R2_START_NOT_ON_BRANCH] : [];
|
|
20468
|
+
}
|
|
20469
|
+
if (attempt2.reason === "cannot-count" && attempt2.phase !== void 0) {
|
|
20470
|
+
return firstNoticeThisSession("observe-no-start-recorded", folder, sessionId) ? [r3NoStartRecorded(attempt2.phase)] : [];
|
|
20471
|
+
}
|
|
20472
|
+
return [];
|
|
20473
|
+
}
|
|
20474
|
+
if (attempt2.outcome === "kept")
|
|
20475
|
+
return [];
|
|
20476
|
+
const { notInPlan, outside } = attempt2.answer;
|
|
20477
|
+
const path = observeNoticesPath(folder, sessionId);
|
|
20478
|
+
const prior = readSaid(path);
|
|
20479
|
+
const notices = [];
|
|
20480
|
+
const parts = {};
|
|
20481
|
+
let grew = false;
|
|
20482
|
+
for (const entry of notInPlan) {
|
|
20483
|
+
const reasons = reasonsOf(entry);
|
|
20484
|
+
parts[entry.part] = reasons;
|
|
20485
|
+
const before = prior.parts[entry.part];
|
|
20486
|
+
if (before === void 0 || reasons.some((reason) => !before.includes(reason)))
|
|
20487
|
+
grew = true;
|
|
20488
|
+
}
|
|
20489
|
+
if (grew)
|
|
20490
|
+
notices.push(r4OutsideThePlan(notInPlan));
|
|
20491
|
+
let outsideFiles = outside.files;
|
|
20492
|
+
if (outside.files > prior.outsideFiles) {
|
|
20493
|
+
if (outside.example !== null)
|
|
20494
|
+
notices.push(r5OutsideEveryPart(outside.files, outside.example));
|
|
20495
|
+
else
|
|
20496
|
+
outsideFiles = prior.outsideFiles;
|
|
20497
|
+
}
|
|
20498
|
+
writeSaid(path, { parts, outsideFiles });
|
|
20499
|
+
return notices;
|
|
20500
|
+
}
|
|
20501
|
+
|
|
18977
20502
|
// dist/hooks/stop.js
|
|
18978
|
-
async function runStop() {
|
|
20503
|
+
async function runStop(observeDeps = {}) {
|
|
18979
20504
|
const input = await readHookStdin();
|
|
18980
|
-
await runSessionDiff(input, "stop");
|
|
20505
|
+
await runSessionDiff(input, "stop", readSeatConfiguration, (reply) => observeChanges(input.session_id, reply, observeDeps));
|
|
18981
20506
|
}
|
|
18982
20507
|
|
|
18983
|
-
// dist/hooks/user-prompt-submit.js
|
|
18984
|
-
import { readFileSync as readFileSync8 } from "node:fs";
|
|
18985
|
-
import { homedir as homedir4 } from "node:os";
|
|
18986
|
-
import { join as join8 } from "node:path";
|
|
18987
|
-
|
|
18988
20508
|
// dist/hooks/scope-phrase.js
|
|
18989
20509
|
var APPROVE_PREFIX = /^approve /i;
|
|
18990
20510
|
var SEND_BACK_PREFIX = /^send back /i;
|
|
@@ -19018,6 +20538,128 @@ function matchScopePhraseShape(shape, shown) {
|
|
|
19018
20538
|
return { kind: "show-first", act: "send-back" };
|
|
19019
20539
|
}
|
|
19020
20540
|
|
|
20541
|
+
// dist/hooks/review-phrase.js
|
|
20542
|
+
var VERBS = [
|
|
20543
|
+
[/^accept /i, "accept"],
|
|
20544
|
+
[/^question /i, "question"],
|
|
20545
|
+
[/^send back /i, "send-back"],
|
|
20546
|
+
[/^carry /i, "carry"],
|
|
20547
|
+
[/^drop /i, "drop"]
|
|
20548
|
+
];
|
|
20549
|
+
function reviewPhraseShape(prompt) {
|
|
20550
|
+
if (typeof prompt !== "string")
|
|
20551
|
+
return null;
|
|
20552
|
+
const line2 = prompt.trim();
|
|
20553
|
+
if (LINE_BREAK.test(line2))
|
|
20554
|
+
return null;
|
|
20555
|
+
for (const [prefix, act] of VERBS) {
|
|
20556
|
+
const verb = prefix.exec(line2);
|
|
20557
|
+
if (verb === null)
|
|
20558
|
+
continue;
|
|
20559
|
+
const rest = line2.slice(verb[0].length);
|
|
20560
|
+
if (act === "accept") {
|
|
20561
|
+
if (!rest.includes("/"))
|
|
20562
|
+
return isValidPhaseIdentity(rest) ? { kind: "phase", reference: rest } : null;
|
|
20563
|
+
return partShape(act, rest, null);
|
|
20564
|
+
}
|
|
20565
|
+
const colon = rest.indexOf(":");
|
|
20566
|
+
if (colon === -1)
|
|
20567
|
+
return act === "send-back" ? partShape(act, rest, null) : null;
|
|
20568
|
+
return partShape(act, rest.slice(0, colon), rest.slice(colon + 1).trim());
|
|
20569
|
+
}
|
|
20570
|
+
return null;
|
|
20571
|
+
}
|
|
20572
|
+
function partShape(act, head, words) {
|
|
20573
|
+
const slash = head.indexOf("/");
|
|
20574
|
+
if (slash === -1)
|
|
20575
|
+
return null;
|
|
20576
|
+
const reference = head.slice(0, slash);
|
|
20577
|
+
const part = head.slice(slash + 1);
|
|
20578
|
+
if (!isValidPhaseIdentity(reference) || !SYSTEM_PART_ID_PATTERN.test(part))
|
|
20579
|
+
return null;
|
|
20580
|
+
return { kind: "part", act, reference, part, words };
|
|
20581
|
+
}
|
|
20582
|
+
function matchReviewPhraseShape(shape, reviews) {
|
|
20583
|
+
const entry = resolvePhaseReference(shape.reference, reviews);
|
|
20584
|
+
if (shape.kind === "phase") {
|
|
20585
|
+
if (entry === null)
|
|
20586
|
+
return { kind: "nothing" };
|
|
20587
|
+
return {
|
|
20588
|
+
kind: "accept",
|
|
20589
|
+
entry,
|
|
20590
|
+
reference: shape.reference,
|
|
20591
|
+
parts: entry.parts.filter((p) => p.built && p.decidable)
|
|
20592
|
+
};
|
|
20593
|
+
}
|
|
20594
|
+
if (entry === null)
|
|
20595
|
+
return { kind: "show-first", act: shape.act };
|
|
20596
|
+
const part = entry.parts.find((p) => p.part === shape.part);
|
|
20597
|
+
if (part === void 0)
|
|
20598
|
+
return { kind: "not-listed", act: shape.act, entry, part: shape.part };
|
|
20599
|
+
return { kind: "decide", act: shape.act, entry, reference: shape.reference, part, words: shape.words };
|
|
20600
|
+
}
|
|
20601
|
+
|
|
20602
|
+
// dist/hooks/shown-record.js
|
|
20603
|
+
import { chmodSync as chmodSync2, readFileSync as readFileSync10, renameSync as renameSync3, unlinkSync as unlinkSync4, writeFileSync as writeFileSync5 } from "node:fs";
|
|
20604
|
+
import { homedir as homedir4, platform as platform2 } from "node:os";
|
|
20605
|
+
import { join as join8 } from "node:path";
|
|
20606
|
+
function recordDir(engagementId, home) {
|
|
20607
|
+
if (!isEngagementId(engagementId))
|
|
20608
|
+
return null;
|
|
20609
|
+
return join8(home ?? homedir4(), HALFCYCLE_DIR_NAME, ENGAGEMENTS_DIR_NAME, engagementId);
|
|
20610
|
+
}
|
|
20611
|
+
function readShownRecord(engagementId, home) {
|
|
20612
|
+
const dir = recordDir(engagementId, home);
|
|
20613
|
+
if (dir === null)
|
|
20614
|
+
return { entries: [], reviews: [] };
|
|
20615
|
+
try {
|
|
20616
|
+
return parseShownRecord(readFileSync10(join8(dir, SHOWN_PLANS_FILENAME), "utf-8"));
|
|
20617
|
+
} catch {
|
|
20618
|
+
return { entries: [], reviews: [] };
|
|
20619
|
+
}
|
|
20620
|
+
}
|
|
20621
|
+
function movedPart(part, act) {
|
|
20622
|
+
if (act === void 0)
|
|
20623
|
+
return part;
|
|
20624
|
+
return {
|
|
20625
|
+
...part,
|
|
20626
|
+
decidable: false,
|
|
20627
|
+
awaitingAnswer: part.awaitingAnswer || act === "question",
|
|
20628
|
+
sentBack: part.sentBack || act === "send-back"
|
|
20629
|
+
};
|
|
20630
|
+
}
|
|
20631
|
+
function moveListedReviewForward(engagementId, home, listed, reviewVersion, decided) {
|
|
20632
|
+
let temp = null;
|
|
20633
|
+
try {
|
|
20634
|
+
const dir = recordDir(engagementId, home);
|
|
20635
|
+
if (dir === null)
|
|
20636
|
+
return false;
|
|
20637
|
+
const path = join8(dir, SHOWN_PLANS_FILENAME);
|
|
20638
|
+
const current = parseShownRecord(readFileSync10(path, "utf-8"));
|
|
20639
|
+
const at = current.reviews.findIndex((r) => r.phase === listed.phase && r.shownAt === listed.shownAt && r.reviewVersion === listed.reviewVersion);
|
|
20640
|
+
if (at === -1)
|
|
20641
|
+
return false;
|
|
20642
|
+
const acts = new Map(decided.map((d) => [d.part, d.act]));
|
|
20643
|
+
const reviews = current.reviews.map((review, i) => i === at ? { ...review, reviewVersion, parts: review.parts.map((p) => movedPart(p, acts.get(p.part))) } : review);
|
|
20644
|
+
const body = serialiseShownRecord({ entries: current.entries, reviews });
|
|
20645
|
+
temp = join8(dir, `.${SHOWN_PLANS_FILENAME}.${process.pid}.tmp`);
|
|
20646
|
+
writeFileSync5(temp, body, { mode: 384 });
|
|
20647
|
+
if (platform2() !== "win32")
|
|
20648
|
+
chmodSync2(temp, 384);
|
|
20649
|
+
renameSync3(temp, path);
|
|
20650
|
+
return true;
|
|
20651
|
+
} catch {
|
|
20652
|
+
if (temp !== null) {
|
|
20653
|
+
try {
|
|
20654
|
+
unlinkSync4(temp);
|
|
20655
|
+
} catch {
|
|
20656
|
+
return false;
|
|
20657
|
+
}
|
|
20658
|
+
}
|
|
20659
|
+
return false;
|
|
20660
|
+
}
|
|
20661
|
+
}
|
|
20662
|
+
|
|
19021
20663
|
// dist/hooks/user-prompt-submit-lines.js
|
|
19022
20664
|
var TELL_THE_PERSON = "Tell the person.";
|
|
19023
20665
|
function line(text) {
|
|
@@ -19038,6 +20680,9 @@ function notAttendedLine(act) {
|
|
|
19038
20680
|
function notSignedInLine(act) {
|
|
19039
20681
|
return line(`${nothingDone(act)}: this project is not signed in on this computer. ${HOOK_REMEDY}`);
|
|
19040
20682
|
}
|
|
20683
|
+
function notOwnerLine(act) {
|
|
20684
|
+
return line(`${nothingDone(act)}: only the project's owner can approve or send back this scope.`);
|
|
20685
|
+
}
|
|
19041
20686
|
function showFirstLine(act) {
|
|
19042
20687
|
return line(`${nothingDone(act)}: that phase has not been listed here. List its parts first with \`${HALFCYCLE_COMMAND} plan <phase> --show\`, then type the line again.`);
|
|
19043
20688
|
}
|
|
@@ -19075,7 +20720,7 @@ function lineForAnswer(act, phase, answer) {
|
|
|
19075
20720
|
case "build-started":
|
|
19076
20721
|
return line(`${nothingDone(act)}: tasks in ${phase} have started, so its scope can no longer be sent back. A built part goes back at the phase's review.`);
|
|
19077
20722
|
case "not-owner":
|
|
19078
|
-
return
|
|
20723
|
+
return notOwnerLine(act);
|
|
19079
20724
|
case "no-owner":
|
|
19080
20725
|
return line(`${nothingDone(act)}: this project has no owner, so nobody can approve or send back its scope.`);
|
|
19081
20726
|
case "not-offered":
|
|
@@ -19090,14 +20735,210 @@ function lineForAnswer(act, phase, answer) {
|
|
|
19090
20735
|
}
|
|
19091
20736
|
}
|
|
19092
20737
|
}
|
|
20738
|
+
var NOTHING_RECORDED = "Nothing was recorded";
|
|
20739
|
+
var LIST_AGAIN = 'Say "review what was built" to list it again';
|
|
20740
|
+
function reviewNotAttendedLine() {
|
|
20741
|
+
return line(`${NOTHING_RECORDED}: reviewing by typing counts only when a person types it into an open session.`);
|
|
20742
|
+
}
|
|
20743
|
+
function reviewNotSignedInLine() {
|
|
20744
|
+
return line(`${NOTHING_RECORDED}: this project is not signed in on this computer. ${HOOK_REMEDY}`);
|
|
20745
|
+
}
|
|
20746
|
+
function reviewCannotReachLine() {
|
|
20747
|
+
return line(`${NOTHING_RECORDED}: Halfcycle could not be reached just now. Type the line again in a minute.`);
|
|
20748
|
+
}
|
|
20749
|
+
function reviewSwitchedOffLine() {
|
|
20750
|
+
return line(`${NOTHING_RECORDED}. ${TYPED_REVIEW_OFF_SENTENCE}`);
|
|
20751
|
+
}
|
|
20752
|
+
function reviewNotOwnerLine() {
|
|
20753
|
+
return line(`${NOTHING_RECORDED}: only the project's owner can review this phase.`);
|
|
20754
|
+
}
|
|
20755
|
+
function reviewNoOwnerLine() {
|
|
20756
|
+
return line(`${NOTHING_RECORDED}: this project has no owner, so nobody can review its phases.`);
|
|
20757
|
+
}
|
|
20758
|
+
function reviewWrongActLine(name, built) {
|
|
20759
|
+
return line(built ? `${NOTHING_RECORDED}: "${name}" was built, so it can be accepted, questioned or sent back.` : `${NOTHING_RECORDED}: "${name}" was not built, so it can only be carried or dropped.`);
|
|
20760
|
+
}
|
|
20761
|
+
function wordsActNoun(act) {
|
|
20762
|
+
switch (act) {
|
|
20763
|
+
case "question":
|
|
20764
|
+
return "a question";
|
|
20765
|
+
case "send-back":
|
|
20766
|
+
return "a send-back";
|
|
20767
|
+
case "carry":
|
|
20768
|
+
return "a carry";
|
|
20769
|
+
case "drop":
|
|
20770
|
+
return "a drop";
|
|
20771
|
+
default: {
|
|
20772
|
+
const unreachable = act;
|
|
20773
|
+
return unreachable;
|
|
20774
|
+
}
|
|
20775
|
+
}
|
|
20776
|
+
}
|
|
20777
|
+
function reviewNoWordsLine(act) {
|
|
20778
|
+
return line(`${NOTHING_RECORDED}: ${wordsActNoun(act)} needs words after the colon.`);
|
|
20779
|
+
}
|
|
20780
|
+
function reviewWordsTooLongLine() {
|
|
20781
|
+
return line(`${NOTHING_RECORDED}: the words after the colon are too long. Shorten them and type the line again.`);
|
|
20782
|
+
}
|
|
20783
|
+
function reviewWordsLookSecretLine() {
|
|
20784
|
+
return line(`${NOTHING_RECORDED}: the words look like they hold a password or key, so they never left this computer. Type the line again without it.`);
|
|
20785
|
+
}
|
|
20786
|
+
function reviewShowFirstLine() {
|
|
20787
|
+
return line(`${NOTHING_RECORDED}: that review has not been listed here. Say "review what was built" to list it, then type the line again.`);
|
|
20788
|
+
}
|
|
20789
|
+
function reviewPartNotListedLine(part, phase) {
|
|
20790
|
+
return line(`${NOTHING_RECORDED}: "${part}" is not a part of the review of ${phase} as it was listed here.`);
|
|
20791
|
+
}
|
|
20792
|
+
function reviewNothingToAcceptLine(phase) {
|
|
20793
|
+
return line(`${NOTHING_RECORDED}: no built part of ${phase} waits for your decision.`);
|
|
20794
|
+
}
|
|
20795
|
+
function reviewListedTooLongAgoLine(phase) {
|
|
20796
|
+
return line(`${NOTHING_RECORDED}: ${phase} was listed an hour or more ago. ${LIST_AGAIN}, then type the line again within the hour.`);
|
|
20797
|
+
}
|
|
20798
|
+
function reviewChangedLine(phase) {
|
|
20799
|
+
return line(`${NOTHING_RECORDED}: the review of ${phase} changed after it was listed here. ${LIST_AGAIN}, then type the line again.`);
|
|
20800
|
+
}
|
|
20801
|
+
function reviewAlreadyDecidedLine(name) {
|
|
20802
|
+
return line(`${NOTHING_RECORDED}: "${name}" is already decided in this review.`);
|
|
20803
|
+
}
|
|
20804
|
+
function reviewAwaitingAnswerLine(name) {
|
|
20805
|
+
return line(`${NOTHING_RECORDED}: "${name}" is waiting for the agent's answer to your question. Once it is in, say "review what was built" to list it again and decide the part.`);
|
|
20806
|
+
}
|
|
20807
|
+
function reviewSentBackLine(name) {
|
|
20808
|
+
return line(`${NOTHING_RECORDED}: "${name}" was sent back to building in this round, so it waits for the next round.`);
|
|
20809
|
+
}
|
|
20810
|
+
function reviewWalkChangedLine(phase, number4) {
|
|
20811
|
+
const typed = typedPhaseReference(phase, number4);
|
|
20812
|
+
return line(`${NOTHING_RECORDED}: phase ${phase} cannot close, because its walk was recorded as clean and a defect is now recorded against it. Send the part with the defect back with send back ${typed}/<part>: <what is wrong>; when its tasks are finished, the close step opens the review again with the walk's defects.`);
|
|
20813
|
+
}
|
|
20814
|
+
function reviewNoReviewLine(phase) {
|
|
20815
|
+
return line(`${NOTHING_RECORDED}: phase ${phase} has no review open.`);
|
|
20816
|
+
}
|
|
20817
|
+
function reviewCloseRefusedLine(phase, steps) {
|
|
20818
|
+
if (steps.length === 0) {
|
|
20819
|
+
return line(`${NOTHING_RECORDED}: phase ${phase} could not close just now. Type the line again in a minute.`);
|
|
20820
|
+
}
|
|
20821
|
+
const labels = steps.map(unfinishedStepLabel).join(", ");
|
|
20822
|
+
const head = steps.length === 1 ? `${NOTHING_RECORDED}: phase ${phase} cannot close while 1 of its steps is unfinished: ${labels}.` : `${NOTHING_RECORDED}: phase ${phase} cannot close while ${steps.length} of its steps are unfinished: ${labels}.`;
|
|
20823
|
+
return line([head, ...steps.map(unfinishedStepLine), "Then type the line again."].join(" "));
|
|
20824
|
+
}
|
|
20825
|
+
function reviewRefusedLine(act) {
|
|
20826
|
+
return act === "accept" ? line(`${NOTHING_RECORDED}: Halfcycle did not accept the request. Update Halfcycle by running \`${HALFCYCLE_COMMAND}\` in this project's folder, then type the line again.`) : line(`${NOTHING_RECORDED}: the words were not accepted. Reword them in plain words and type the line again.`);
|
|
20827
|
+
}
|
|
20828
|
+
function stillWaitingOf(phase, waiting) {
|
|
20829
|
+
if (waiting === 0)
|
|
20830
|
+
return `No part of ${phase} waits for your decision now.`;
|
|
20831
|
+
if (waiting === 1)
|
|
20832
|
+
return `1 part of ${phase} still waits for your decision.`;
|
|
20833
|
+
return `${waiting} parts of ${phase} still wait for your decision.`;
|
|
20834
|
+
}
|
|
20835
|
+
function stillWaiting(waiting) {
|
|
20836
|
+
if (waiting === 0)
|
|
20837
|
+
return "No part waits for your decision now.";
|
|
20838
|
+
if (waiting === 1)
|
|
20839
|
+
return "1 part still waits for your decision.";
|
|
20840
|
+
return `${waiting} parts still wait for your decision.`;
|
|
20841
|
+
}
|
|
20842
|
+
function decidedWords(decided) {
|
|
20843
|
+
switch (decided.act) {
|
|
20844
|
+
case "accept":
|
|
20845
|
+
return "accepted";
|
|
20846
|
+
case "question":
|
|
20847
|
+
return "questioned";
|
|
20848
|
+
case "send-back":
|
|
20849
|
+
return "sent back to building";
|
|
20850
|
+
case "carry":
|
|
20851
|
+
return decided.carriedTo === void 0 || decided.carriedTo === null ? "carried to the next phase planned" : `carried to phase ${decided.carriedTo.number}, "${decided.carriedTo.phase}"`;
|
|
20852
|
+
case "drop":
|
|
20853
|
+
return "dropped";
|
|
20854
|
+
default: {
|
|
20855
|
+
const unreachable = decided.act;
|
|
20856
|
+
return unreachable;
|
|
20857
|
+
}
|
|
20858
|
+
}
|
|
20859
|
+
}
|
|
20860
|
+
function reviewRecordedLine(decided, phase, waiting) {
|
|
20861
|
+
return line(`Recorded in your session: "${decided.name}" ${decidedWords(decided)}. ${stillWaitingOf(phase, waiting)}`);
|
|
20862
|
+
}
|
|
20863
|
+
function reviewBulkRecordedLine(accepted, phase, waiting) {
|
|
20864
|
+
const first = accepted === 1 ? `Recorded in your session: 1 part of ${phase} accepted.` : `Recorded in your session: ${accepted} parts of ${phase} accepted.`;
|
|
20865
|
+
return line(`${first} ${stillWaiting(waiting)}`);
|
|
20866
|
+
}
|
|
20867
|
+
function reviewClosedLine(phase) {
|
|
20868
|
+
return line(`Recorded in your session. That was the last decision: phase ${phase} is closed, and its Build Record is ready.`);
|
|
20869
|
+
}
|
|
20870
|
+
function reviewNoPartsClosedLine(phase) {
|
|
20871
|
+
return line(`Recorded in your session. Phase ${phase} had no parts to review, so it is closed, and its Build Record is ready.`);
|
|
20872
|
+
}
|
|
20873
|
+
function notDecidableLine(answer) {
|
|
20874
|
+
const first = answer.parts[0];
|
|
20875
|
+
switch (first.why) {
|
|
20876
|
+
case "question-waiting":
|
|
20877
|
+
return reviewAwaitingAnswerLine(first.name);
|
|
20878
|
+
case "sent-back":
|
|
20879
|
+
return reviewSentBackLine(first.name);
|
|
20880
|
+
case "settled":
|
|
20881
|
+
return reviewAlreadyDecidedLine(first.name);
|
|
20882
|
+
default: {
|
|
20883
|
+
const unreachable = first.why;
|
|
20884
|
+
return unreachable;
|
|
20885
|
+
}
|
|
20886
|
+
}
|
|
20887
|
+
}
|
|
20888
|
+
function reviewLineForAnswer(context, answer) {
|
|
20889
|
+
const { phase } = context;
|
|
20890
|
+
switch (answer.outcome) {
|
|
20891
|
+
case "recorded": {
|
|
20892
|
+
if (answer.closed) {
|
|
20893
|
+
return answer.decided.length === 0 ? reviewNoPartsClosedLine(phase) : reviewClosedLine(phase);
|
|
20894
|
+
}
|
|
20895
|
+
if (context.bulk)
|
|
20896
|
+
return reviewBulkRecordedLine(answer.decided.length, phase, answer.waiting);
|
|
20897
|
+
return reviewRecordedLine(answer.decided[0], phase, answer.waiting);
|
|
20898
|
+
}
|
|
20899
|
+
case "review-changed":
|
|
20900
|
+
return reviewChangedLine(phase);
|
|
20901
|
+
case "no-review":
|
|
20902
|
+
return reviewNoReviewLine(phase);
|
|
20903
|
+
case "not-decidable":
|
|
20904
|
+
return notDecidableLine(answer);
|
|
20905
|
+
case "close-refused":
|
|
20906
|
+
return reviewCloseRefusedLine(phase, answer.steps);
|
|
20907
|
+
case "walk-changed":
|
|
20908
|
+
return reviewWalkChangedLine(phase, context.number);
|
|
20909
|
+
case "not-owner":
|
|
20910
|
+
return reviewNotOwnerLine();
|
|
20911
|
+
case "no-owner":
|
|
20912
|
+
return reviewNoOwnerLine();
|
|
20913
|
+
case "refused":
|
|
20914
|
+
return reviewRefusedLine(context.act);
|
|
20915
|
+
case "unavailable":
|
|
20916
|
+
return reviewCannotReachLine();
|
|
20917
|
+
case "show-first":
|
|
20918
|
+
return reviewShowFirstLine();
|
|
20919
|
+
case "not-offered":
|
|
20920
|
+
return reviewSwitchedOffLine();
|
|
20921
|
+
default: {
|
|
20922
|
+
const unreachable = answer;
|
|
20923
|
+
return unreachable;
|
|
20924
|
+
}
|
|
20925
|
+
}
|
|
20926
|
+
}
|
|
19093
20927
|
|
|
19094
20928
|
// dist/hooks/user-prompt-submit.js
|
|
19095
20929
|
var CHAT_REQUEST_TIMEOUT_MS = 6e3;
|
|
19096
20930
|
var SHOWN_ENTRY_MAX_AGE_MS = 60 * 60 * 1e3;
|
|
19097
20931
|
async function runUserPromptSubmit(opts) {
|
|
19098
20932
|
let act = null;
|
|
20933
|
+
const review = { recognised: false };
|
|
19099
20934
|
try {
|
|
19100
20935
|
const prompt = promptOf(opts.input);
|
|
20936
|
+
const reviewShape = reviewPhraseShape(prompt);
|
|
20937
|
+
if (reviewShape !== null) {
|
|
20938
|
+
const out = await decideReview(reviewShape, opts, review);
|
|
20939
|
+
return out === "" ? "" : `${out}
|
|
20940
|
+
`;
|
|
20941
|
+
}
|
|
19101
20942
|
const shape = scopePhraseShape(prompt);
|
|
19102
20943
|
if (shape === null)
|
|
19103
20944
|
return "";
|
|
@@ -19105,25 +20946,130 @@ async function runUserPromptSubmit(opts) {
|
|
|
19105
20946
|
return `${await decide(shape, opts)}
|
|
19106
20947
|
`;
|
|
19107
20948
|
} catch {
|
|
20949
|
+
if (review.recognised)
|
|
20950
|
+
return `${reviewCannotReachLine()}
|
|
20951
|
+
`;
|
|
19108
20952
|
return act === null ? "" : `${cannotReachLine(act)}
|
|
19109
20953
|
`;
|
|
19110
20954
|
}
|
|
19111
20955
|
}
|
|
20956
|
+
async function decideReview(shape, opts, progress) {
|
|
20957
|
+
const env = opts.env ?? process.env;
|
|
20958
|
+
if (shape.kind === "phase") {
|
|
20959
|
+
const read2 = readConfig(env);
|
|
20960
|
+
if (read2.config === null)
|
|
20961
|
+
return "";
|
|
20962
|
+
const match2 = matchReviewPhraseShape(shape, readShownRecord(read2.config.guardEngagementId, opts.home).reviews);
|
|
20963
|
+
if (match2.kind === "nothing")
|
|
20964
|
+
return "";
|
|
20965
|
+
progress.recognised = true;
|
|
20966
|
+
if (!readSessionAttended(env))
|
|
20967
|
+
return reviewNotAttendedLine();
|
|
20968
|
+
return checkAndSendReview(match2, read2.config.guardEngagementId, read2.config.guardServiceToken, env, opts);
|
|
20969
|
+
}
|
|
20970
|
+
progress.recognised = true;
|
|
20971
|
+
if (!readSessionAttended(env))
|
|
20972
|
+
return reviewNotAttendedLine();
|
|
20973
|
+
const read = readConfig(env);
|
|
20974
|
+
if (read.config === null) {
|
|
20975
|
+
return readNeverConfiguredReason(env) === "not-this-account" ? reviewNotOwnerLine() : reviewNotSignedInLine();
|
|
20976
|
+
}
|
|
20977
|
+
const match = matchReviewPhraseShape(shape, readShownRecord(read.config.guardEngagementId, opts.home).reviews);
|
|
20978
|
+
return checkAndSendReview(match, read.config.guardEngagementId, read.config.guardServiceToken, env, opts);
|
|
20979
|
+
}
|
|
20980
|
+
async function checkAndSendReview(match, engagementId, credential, env, opts) {
|
|
20981
|
+
if (match.kind === "nothing" || match.kind === "show-first")
|
|
20982
|
+
return reviewShowFirstLine();
|
|
20983
|
+
if (match.kind === "not-listed")
|
|
20984
|
+
return reviewPartNotListedLine(match.part, match.entry.phase);
|
|
20985
|
+
const { entry } = match;
|
|
20986
|
+
if (!listedRecently(entry.shownAt, (opts.now ?? Date.now)()))
|
|
20987
|
+
return reviewListedTooLongAgoLine(entry.phase);
|
|
20988
|
+
let candidate;
|
|
20989
|
+
if (match.kind === "accept") {
|
|
20990
|
+
if (entry.parts.length > 0 && match.parts.length === 0)
|
|
20991
|
+
return reviewNothingToAcceptLine(entry.phase);
|
|
20992
|
+
candidate = {
|
|
20993
|
+
act: "accept",
|
|
20994
|
+
reference: match.reference,
|
|
20995
|
+
reviewVersion: entry.reviewVersion,
|
|
20996
|
+
parts: match.parts.map((p) => p.part)
|
|
20997
|
+
};
|
|
20998
|
+
} else {
|
|
20999
|
+
const { act, part } = match;
|
|
21000
|
+
if (!reviewActsFor(part.built).includes(act))
|
|
21001
|
+
return reviewWrongActLine(part.name, part.built);
|
|
21002
|
+
let words = "";
|
|
21003
|
+
if (act !== "accept") {
|
|
21004
|
+
words = match.words ?? "";
|
|
21005
|
+
if (words === "")
|
|
21006
|
+
return reviewNoWordsLine(act);
|
|
21007
|
+
if (!reviewNoteWireSchema.safeParse(words).success)
|
|
21008
|
+
return reviewWordsTooLongLine();
|
|
21009
|
+
if (looksLikeSecret(words))
|
|
21010
|
+
return reviewWordsLookSecretLine();
|
|
21011
|
+
}
|
|
21012
|
+
if (part.awaitingAnswer)
|
|
21013
|
+
return reviewAwaitingAnswerLine(part.name);
|
|
21014
|
+
if (part.sentBack)
|
|
21015
|
+
return reviewSentBackLine(part.name);
|
|
21016
|
+
if (!part.decidable)
|
|
21017
|
+
return reviewAlreadyDecidedLine(part.name);
|
|
21018
|
+
candidate = act === "accept" ? { act, reference: match.reference, reviewVersion: entry.reviewVersion, parts: [part.part] } : { act, reference: match.reference, reviewVersion: entry.reviewVersion, part: part.part, note: words };
|
|
21019
|
+
}
|
|
21020
|
+
const request = chatReviewRequestSchema.safeParse(candidate);
|
|
21021
|
+
if (!request.success)
|
|
21022
|
+
return reviewShowFirstLine();
|
|
21023
|
+
return postReview(match.kind === "accept", entry, request.data, engagementId, credential, env, opts);
|
|
21024
|
+
}
|
|
21025
|
+
async function postReview(bulk, entry, request, engagementId, credential, env, opts) {
|
|
21026
|
+
const fetchFn = opts.fetchFn ?? globalThis.fetch;
|
|
21027
|
+
const url2 = `${readControlOrigin(env).origin}/engagements/${encodeURIComponent(engagementId)}/phases/${encodeURIComponent(entry.phase)}/review/chat`;
|
|
21028
|
+
const controller = new AbortController();
|
|
21029
|
+
const timer = setTimeout(() => controller.abort(), CHAT_REQUEST_TIMEOUT_MS);
|
|
21030
|
+
try {
|
|
21031
|
+
const response = await fetchFn(url2, {
|
|
21032
|
+
method: "POST",
|
|
21033
|
+
headers: { Authorization: `Bearer ${credential}`, "Content-Type": "application/json" },
|
|
21034
|
+
body: JSON.stringify(request),
|
|
21035
|
+
signal: controller.signal
|
|
21036
|
+
});
|
|
21037
|
+
if (response.status === 401)
|
|
21038
|
+
return reviewNotSignedInLine();
|
|
21039
|
+
if (response.status === 429 || response.status >= 500)
|
|
21040
|
+
return reviewCannotReachLine();
|
|
21041
|
+
const parsed = chatReviewResultSchema.safeParse(await response.json());
|
|
21042
|
+
if (!parsed.success)
|
|
21043
|
+
return reviewCannotReachLine();
|
|
21044
|
+
const answer = parsed.data;
|
|
21045
|
+
if (answer.outcome === "recorded") {
|
|
21046
|
+
if (response.status !== 200)
|
|
21047
|
+
return reviewCannotReachLine();
|
|
21048
|
+
moveListedReviewForward(engagementId, opts.home, entry, answer.reviewVersion, answer.decided);
|
|
21049
|
+
}
|
|
21050
|
+
return reviewLineForAnswer({ act: request.act, bulk, phase: entry.phase, number: entry.number }, answer);
|
|
21051
|
+
} catch {
|
|
21052
|
+
return reviewCannotReachLine();
|
|
21053
|
+
} finally {
|
|
21054
|
+
clearTimeout(timer);
|
|
21055
|
+
}
|
|
21056
|
+
}
|
|
19112
21057
|
async function decide(shape, opts) {
|
|
19113
21058
|
const { act } = shape;
|
|
19114
21059
|
const env = opts.env ?? process.env;
|
|
19115
21060
|
if (!readSessionAttended(env))
|
|
19116
21061
|
return notAttendedLine(act);
|
|
19117
21062
|
const read = readConfig(env);
|
|
19118
|
-
if (read.config === null)
|
|
19119
|
-
return notSignedInLine(act);
|
|
21063
|
+
if (read.config === null) {
|
|
21064
|
+
return readNeverConfiguredReason(env) === "not-this-account" ? notOwnerLine(act) : notSignedInLine(act);
|
|
21065
|
+
}
|
|
19120
21066
|
const { guardServiceToken, guardEngagementId } = read.config;
|
|
19121
|
-
const shown =
|
|
21067
|
+
const shown = readShownRecord(guardEngagementId, opts.home).entries;
|
|
19122
21068
|
const match = matchScopePhraseShape(shape, shown);
|
|
19123
21069
|
if (match.kind === "show-first")
|
|
19124
21070
|
return showFirstLine(act);
|
|
19125
21071
|
const { entry } = match;
|
|
19126
|
-
if (!
|
|
21072
|
+
if (!listedRecently(entry.shownAt, (opts.now ?? Date.now)()))
|
|
19127
21073
|
return tooOldLine(act, entry.phase);
|
|
19128
21074
|
let candidate;
|
|
19129
21075
|
if (match.kind === "send-back") {
|
|
@@ -19152,19 +21098,8 @@ function promptOf(input) {
|
|
|
19152
21098
|
return void 0;
|
|
19153
21099
|
}
|
|
19154
21100
|
}
|
|
19155
|
-
function
|
|
19156
|
-
|
|
19157
|
-
return [];
|
|
19158
|
-
let text;
|
|
19159
|
-
try {
|
|
19160
|
-
text = readFileSync8(join8(home ?? homedir4(), HALFCYCLE_DIR_NAME, ENGAGEMENTS_DIR_NAME, engagementId, SHOWN_PLANS_FILENAME), "utf-8");
|
|
19161
|
-
} catch {
|
|
19162
|
-
return [];
|
|
19163
|
-
}
|
|
19164
|
-
return parseShownPlans(text);
|
|
19165
|
-
}
|
|
19166
|
-
function shownRecently(entry, now) {
|
|
19167
|
-
const age = now - Date.parse(entry.shownAt);
|
|
21101
|
+
function listedRecently(shownAt, now) {
|
|
21102
|
+
const age = now - Date.parse(shownAt);
|
|
19168
21103
|
return Number.isFinite(age) && age >= 0 && age < SHOWN_ENTRY_MAX_AGE_MS;
|
|
19169
21104
|
}
|
|
19170
21105
|
async function post(act, entry, request, engagementId, credential, env, fetchFn = globalThis.fetch) {
|
|
@@ -19196,7 +21131,7 @@ async function post(act, entry, request, engagementId, credential, env, fetchFn
|
|
|
19196
21131
|
}
|
|
19197
21132
|
|
|
19198
21133
|
// dist/resolve-engagement.js
|
|
19199
|
-
import { existsSync as existsSync6, readFileSync as
|
|
21134
|
+
import { existsSync as existsSync6, readFileSync as readFileSync11, realpathSync as realpathSync3 } from "node:fs";
|
|
19200
21135
|
import { join as join9, sep as sep2 } from "node:path";
|
|
19201
21136
|
import { homedir as homedir5 } from "node:os";
|
|
19202
21137
|
function defaultRegistryPath() {
|
|
@@ -19206,7 +21141,7 @@ function readRegistry(registryPath = defaultRegistryPath()) {
|
|
|
19206
21141
|
if (!existsSync6(registryPath))
|
|
19207
21142
|
return {};
|
|
19208
21143
|
try {
|
|
19209
|
-
const parsed = JSON.parse(
|
|
21144
|
+
const parsed = JSON.parse(readFileSync11(registryPath, "utf-8"));
|
|
19210
21145
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
|
|
19211
21146
|
return {};
|
|
19212
21147
|
const out = {};
|
|
@@ -19254,13 +21189,13 @@ function runResolveEngagement(cwd, registryPath) {
|
|
|
19254
21189
|
}
|
|
19255
21190
|
|
|
19256
21191
|
// dist/refresh-credential.js
|
|
19257
|
-
import { readFileSync as
|
|
19258
|
-
import { resolve as
|
|
21192
|
+
import { readFileSync as readFileSync13 } from "node:fs";
|
|
21193
|
+
import { resolve as resolve4 } from "node:path";
|
|
19259
21194
|
|
|
19260
21195
|
// dist/mcp-headers.js
|
|
19261
|
-
import { readFileSync as
|
|
21196
|
+
import { readFileSync as readFileSync12 } from "node:fs";
|
|
19262
21197
|
import { homedir as homedir6 } from "node:os";
|
|
19263
|
-
import { dirname as dirname4, resolve as
|
|
21198
|
+
import { dirname as dirname4, resolve as resolve3 } from "node:path";
|
|
19264
21199
|
function defaultWriteErr(text) {
|
|
19265
21200
|
process.stderr.write(text);
|
|
19266
21201
|
}
|
|
@@ -19273,28 +21208,12 @@ function headerLine(bearer, clientVersion) {
|
|
|
19273
21208
|
}
|
|
19274
21209
|
function projectRootFromBinary(binaryPath) {
|
|
19275
21210
|
const depth = VENDORED_BIN_REL.split("/").length - 1;
|
|
19276
|
-
return
|
|
19277
|
-
}
|
|
19278
|
-
function readPinnedId(root) {
|
|
19279
|
-
let raw;
|
|
19280
|
-
try {
|
|
19281
|
-
raw = readFileSync10(resolve2(root, BUNDLE_PIN_REL), "utf-8");
|
|
19282
|
-
} catch {
|
|
19283
|
-
return { problem: "no-pin" };
|
|
19284
|
-
}
|
|
19285
|
-
let pin;
|
|
19286
|
-
try {
|
|
19287
|
-
pin = JSON.parse(raw);
|
|
19288
|
-
} catch {
|
|
19289
|
-
return { problem: "no-id" };
|
|
19290
|
-
}
|
|
19291
|
-
const id = typeof pin === "object" && pin !== null ? pin[PIN_ENGAGEMENT_ID_FIELD] : void 0;
|
|
19292
|
-
return typeof id === "string" && id !== "" ? { id } : { problem: "no-id" };
|
|
21211
|
+
return resolve3(dirname4(binaryPath), ...Array(depth).fill(".."));
|
|
19293
21212
|
}
|
|
19294
21213
|
function readPinnedVersion(root) {
|
|
19295
21214
|
let raw;
|
|
19296
21215
|
try {
|
|
19297
|
-
raw =
|
|
21216
|
+
raw = readFileSync12(resolve3(root, BUNDLE_PIN_REL), "utf-8");
|
|
19298
21217
|
} catch {
|
|
19299
21218
|
return void 0;
|
|
19300
21219
|
}
|
|
@@ -19336,7 +21255,7 @@ function sentences(lines) {
|
|
|
19336
21255
|
}
|
|
19337
21256
|
function badIdSentence(root) {
|
|
19338
21257
|
return sentences([
|
|
19339
|
-
`${
|
|
21258
|
+
`${resolve3(root, BUNDLE_PIN_REL)} names an engagement id that is not a Halfcycle project id,`,
|
|
19340
21259
|
'so no credential was read. Delete that file and run "npx halfcycle" again: this repository',
|
|
19341
21260
|
"is then set up as a new Halfcycle project."
|
|
19342
21261
|
]);
|
|
@@ -19350,7 +21269,7 @@ function storeProblemSentence(problem, root, home, engagementId) {
|
|
|
19350
21269
|
]);
|
|
19351
21270
|
case "no-id":
|
|
19352
21271
|
return sentences([
|
|
19353
|
-
`${
|
|
21272
|
+
`${resolve3(root, BUNDLE_PIN_REL)} names no engagement id.`,
|
|
19354
21273
|
'run "npx halfcycle" here to rewrite it.'
|
|
19355
21274
|
]);
|
|
19356
21275
|
case "bad-id":
|
|
@@ -19359,13 +21278,15 @@ function storeProblemSentence(problem, root, home, engagementId) {
|
|
|
19359
21278
|
return sentences([
|
|
19360
21279
|
"this project belongs to another Halfcycle account, so this machine has no",
|
|
19361
21280
|
"credential for it and the Halfcycle tools are not available here. Nothing is",
|
|
19362
|
-
"broken
|
|
19363
|
-
|
|
21281
|
+
"broken.",
|
|
21282
|
+
requestAccessSentence(engagementId),
|
|
21283
|
+
"If this account is wrong, sign in as the account that owns it and run",
|
|
21284
|
+
'"npx halfcycle" again.'
|
|
19364
21285
|
]);
|
|
19365
21286
|
case "no-credential":
|
|
19366
21287
|
return sentences([
|
|
19367
21288
|
`this machine holds no credential for engagement ${engagementId ?? ""}.`,
|
|
19368
|
-
`run "npx halfcycle" in this repository \u2014 it will sign you in through your browser if needed, and write the credential to ${
|
|
21289
|
+
`run "npx halfcycle" in this repository \u2014 it will sign you in through your browser if needed, and write the credential to ${resolve3(home, HALFCYCLE_DIR_NAME)}, outside this repository.`
|
|
19369
21290
|
]);
|
|
19370
21291
|
}
|
|
19371
21292
|
}
|
|
@@ -19518,7 +21439,7 @@ var INITIALIZE_BODY = JSON.stringify({ jsonrpc: "2.0", id: 1, method: "initializ
|
|
|
19518
21439
|
function registeredMcpUrl(projectRoot) {
|
|
19519
21440
|
let parsed;
|
|
19520
21441
|
try {
|
|
19521
|
-
parsed = JSON.parse(
|
|
21442
|
+
parsed = JSON.parse(readFileSync13(resolve4(projectRoot, MCP_REGISTRATION_REL), "utf-8"));
|
|
19522
21443
|
} catch {
|
|
19523
21444
|
return void 0;
|
|
19524
21445
|
}
|
|
@@ -19625,7 +21546,7 @@ function positionRequestBody(engagementId) {
|
|
|
19625
21546
|
params: { name: "halfcycle_position", arguments: { engagementId } }
|
|
19626
21547
|
});
|
|
19627
21548
|
}
|
|
19628
|
-
async function
|
|
21549
|
+
async function fetchPositionNotices(url2, bearer, engagementId, fetchFn, deadlineMs) {
|
|
19629
21550
|
const controller = new AbortController();
|
|
19630
21551
|
const timer = setTimeout(() => controller.abort(), deadlineMs);
|
|
19631
21552
|
try {
|
|
@@ -19672,9 +21593,11 @@ async function fetchScopeNotices(url2, bearer, engagementId, fetchFn, deadlineMs
|
|
|
19672
21593
|
} catch {
|
|
19673
21594
|
return void 0;
|
|
19674
21595
|
}
|
|
19675
|
-
|
|
19676
|
-
|
|
19677
|
-
|
|
21596
|
+
if (typeof body !== "object" || body === null)
|
|
21597
|
+
return void 0;
|
|
21598
|
+
const scopeParsed = scopeNoticesSchema.safeParse(body["scope"]);
|
|
21599
|
+
const reviewParsed = reviewNoticesSchema.safeParse(body["review"]);
|
|
21600
|
+
return { scope: scopeParsed.success ? scopeParsed.data : void 0, review: reviewParsed.success ? reviewParsed.data : void 0 };
|
|
19678
21601
|
} finally {
|
|
19679
21602
|
clearTimeout(timer);
|
|
19680
21603
|
}
|
|
@@ -19711,9 +21634,11 @@ async function runRefreshCredential(opts) {
|
|
|
19711
21634
|
const remainingForMargin = SESSION_START_HOOK_TIMEOUT_MS - SESSION_START_MARGIN_MS - (Date.now() - startedAt);
|
|
19712
21635
|
const positionDeadline = Math.min(opts.scopeNoticeDeadlineMs ?? SCOPE_NOTICE_DEADLINE_MS, remainingForMargin);
|
|
19713
21636
|
if (positionDeadline > 0) {
|
|
19714
|
-
const
|
|
19715
|
-
if (scope !== void 0)
|
|
19716
|
-
parts.push(...scopeNoticeLines(scope));
|
|
21637
|
+
const notices = await fetchPositionNotices(connection.url, connection.bearer, engagementId, opts.fetchFn ?? globalThis.fetch, positionDeadline);
|
|
21638
|
+
if (notices?.scope !== void 0)
|
|
21639
|
+
parts.push(...scopeNoticeLines(notices.scope));
|
|
21640
|
+
if (notices?.review !== void 0)
|
|
21641
|
+
parts.push(...reviewNoticeText(notices.review));
|
|
19717
21642
|
}
|
|
19718
21643
|
}
|
|
19719
21644
|
} else if (connection.sentence === CANNOT_REACH_SENTENCE || connection.sentence === SERVICE_PROBLEM_SENTENCE) {
|
|
@@ -19723,7 +21648,7 @@ async function runRefreshCredential(opts) {
|
|
|
19723
21648
|
}
|
|
19724
21649
|
if (readConfig().config === null) {
|
|
19725
21650
|
const remedySaid = parts.some(namesTheSignInCommand);
|
|
19726
|
-
parts.push(noCoverageStatement(readNeverConfiguredReason(), remedySaid));
|
|
21651
|
+
parts.push(noCoverageStatement(readNeverConfiguredReason(), remedySaid, pinnedEngagementId(projectRootFromBinary(opts.binaryPath))));
|
|
19727
21652
|
}
|
|
19728
21653
|
for (const said of renewalSaid) {
|
|
19729
21654
|
if (namesTheSignInCommand(said) && parts.some(namesTheSignInCommand))
|
|
@@ -19747,7 +21672,7 @@ async function runRefreshCredential(opts) {
|
|
|
19747
21672
|
import { fileURLToPath } from "node:url";
|
|
19748
21673
|
|
|
19749
21674
|
// dist/capture.js
|
|
19750
|
-
import { readFileSync as
|
|
21675
|
+
import { readFileSync as readFileSync14, writeFileSync as writeFileSync6, mkdirSync as mkdirSync3, existsSync as existsSync7 } from "node:fs";
|
|
19751
21676
|
import { dirname as dirname5 } from "node:path";
|
|
19752
21677
|
function runCapture(provider, endpoint, opts = {}) {
|
|
19753
21678
|
const repoRoot = opts.repoRoot ?? process.cwd();
|
|
@@ -19784,7 +21709,7 @@ function runCapture(provider, endpoint, opts = {}) {
|
|
|
19784
21709
|
const manifestFile = manifestPath(repoRoot);
|
|
19785
21710
|
try {
|
|
19786
21711
|
mkdirSync3(dirname5(fixture), { recursive: true });
|
|
19787
|
-
|
|
21712
|
+
writeFileSync6(fixture, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
|
|
19788
21713
|
} catch (err) {
|
|
19789
21714
|
const msg = err instanceof Error ? err.message : String(err);
|
|
19790
21715
|
return { ok: false, kind: "write-failed", message: `could not write fixture: ${msg}` };
|
|
@@ -19815,7 +21740,7 @@ function readInputFile(fromFile) {
|
|
|
19815
21740
|
if (!fromFile)
|
|
19816
21741
|
return void 0;
|
|
19817
21742
|
try {
|
|
19818
|
-
return
|
|
21743
|
+
return readFileSync14(fromFile, "utf-8");
|
|
19819
21744
|
} catch {
|
|
19820
21745
|
return void 0;
|
|
19821
21746
|
}
|
|
@@ -19839,6 +21764,17 @@ async function main() {
|
|
|
19839
21764
|
process.stdout.write(reply, () => process.exit(0));
|
|
19840
21765
|
return;
|
|
19841
21766
|
}
|
|
21767
|
+
if (cmd === "observe") {
|
|
21768
|
+
let line2;
|
|
21769
|
+
try {
|
|
21770
|
+
line2 = JSON.stringify(await runObserve());
|
|
21771
|
+
} catch {
|
|
21772
|
+
line2 = JSON.stringify({ outcome: "not-sent", reason: "unreachable" });
|
|
21773
|
+
}
|
|
21774
|
+
process.stdout.write(`${line2}
|
|
21775
|
+
`, () => process.exit(0));
|
|
21776
|
+
return;
|
|
21777
|
+
}
|
|
19842
21778
|
if (cmd === "mcp-headers") {
|
|
19843
21779
|
const line2 = await mcpHeaders({ binaryPath: fileURLToPath(import.meta.url) });
|
|
19844
21780
|
process.stdout.write(line2, () => process.exit(0));
|
|
@@ -19921,16 +21857,17 @@ Commit the fixture; the next guard evaluation of this endpoint will pass.
|
|
|
19921
21857
|
halfcycle-runner hook user-prompt-submit
|
|
19922
21858
|
halfcycle-runner refresh-credential
|
|
19923
21859
|
halfcycle-runner mcp-headers
|
|
21860
|
+
halfcycle-runner observe
|
|
19924
21861
|
halfcycle-runner resolve-engagement <cwd>
|
|
19925
21862
|
halfcycle-runner capture <provider> <endpoint> [--from <file>]
|
|
19926
21863
|
`);
|
|
19927
21864
|
process.exit(1);
|
|
19928
21865
|
}
|
|
19929
21866
|
function readStdin3() {
|
|
19930
|
-
return new Promise((
|
|
21867
|
+
return new Promise((resolve5, reject) => {
|
|
19931
21868
|
const chunks = [];
|
|
19932
21869
|
process.stdin.on("data", (chunk) => chunks.push(chunk));
|
|
19933
|
-
process.stdin.on("end", () =>
|
|
21870
|
+
process.stdin.on("end", () => resolve5(Buffer.concat(chunks).toString("utf-8")));
|
|
19934
21871
|
process.stdin.on("error", reject);
|
|
19935
21872
|
});
|
|
19936
21873
|
}
|