halfcycle 0.4.6 → 0.4.7
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 +1325 -155
- package/commands/agent-teams-orchestration.md +1 -1
- package/commands/stack-assembly-worker.md +1 -1
- package/commands/task-execution.md +1 -1
- package/dist/bin.d.ts +2 -1
- package/dist/bin.d.ts.map +1 -1
- package/dist/cli-contract.d.ts +1 -1
- package/dist/cli-contract.d.ts.map +1 -1
- package/dist/cli.js +1460 -417
- package/dist/cli.js.map +3 -3
- package/dist/close-phase.d.ts +8 -1
- package/dist/close-phase.d.ts.map +1 -1
- package/dist/draw/index.d.ts +76 -32
- package/dist/draw/index.d.ts.map +1 -1
- package/dist/draw/write.d.ts +71 -21
- package/dist/draw/write.d.ts.map +1 -1
- package/dist/index.js +69 -6
- package/dist/index.js.map +3 -3
- package/dist/install.d.ts +33 -1
- package/dist/install.d.ts.map +1 -1
- package/dist/open-phase.d.ts +8 -1
- package/dist/open-phase.d.ts.map +1 -1
- package/dist/plan.d.ts +200 -0
- package/dist/plan.d.ts.map +1 -0
- package/dist/system-report.d.ts +20 -9
- package/dist/system-report.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// dist/bin.js
|
|
4
|
-
import { existsSync as existsSync7, readFileSync as
|
|
4
|
+
import { existsSync as existsSync7, readFileSync as readFileSync14, realpathSync as realpathSync4, statSync, writeSync as writeSync2 } from "node:fs";
|
|
5
5
|
import { homedir as homedir3 } from "node:os";
|
|
6
|
-
import { join as
|
|
6
|
+
import { join as join15 } from "node:path";
|
|
7
7
|
|
|
8
8
|
// dist/install.js
|
|
9
9
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
@@ -143,6 +143,7 @@ var VENDORED_BIN_REL = ".halfcycle/bin/bin.bundle.mjs";
|
|
|
143
143
|
var SESSION_START_MARKER_REL = ".claude/hooks/session-start-marker.sh";
|
|
144
144
|
var GUARD_RUNNER_REL = ".claude/hooks/guard-runner.sh";
|
|
145
145
|
var USER_PROMPT_REMINDER_REL = ".claude/hooks/user-prompt-reminder.sh";
|
|
146
|
+
var USER_PROMPT_SCOPE_REL = ".claude/hooks/user-prompt-scope.sh";
|
|
146
147
|
var SESSION_END_MARKER_REL = ".claude/hooks/session-end-marker.sh";
|
|
147
148
|
var WRITE_CAPABLE_TOOLS = ["Edit", "MultiEdit", "NotebookEdit", "Write"];
|
|
148
149
|
function hookCommand(script, verb) {
|
|
@@ -171,6 +172,13 @@ var HALFCYCLE_HOOK_WIRING = [
|
|
|
171
172
|
command: hookCommand(USER_PROMPT_REMINDER_REL),
|
|
172
173
|
script: USER_PROMPT_REMINDER_REL
|
|
173
174
|
},
|
|
175
|
+
{
|
|
176
|
+
event: "UserPromptSubmit",
|
|
177
|
+
matcher: null,
|
|
178
|
+
timeout: 10,
|
|
179
|
+
command: hookCommand(USER_PROMPT_SCOPE_REL),
|
|
180
|
+
script: USER_PROMPT_SCOPE_REL
|
|
181
|
+
},
|
|
174
182
|
{
|
|
175
183
|
event: "PostToolUse",
|
|
176
184
|
matcher: WRITE_CAPABLE_TOOLS.join("|"),
|
|
@@ -252,6 +260,131 @@ function quoteForShell(value) {
|
|
|
252
260
|
}
|
|
253
261
|
var HALFCYCLE_COMMAND = "npx halfcycle@latest";
|
|
254
262
|
|
|
263
|
+
// ../core/dist/shown-plans.js
|
|
264
|
+
var SHOWN_PLANS_FILENAME = "shown-plans.json";
|
|
265
|
+
var SHOWN_PLANS_FORMAT = 1;
|
|
266
|
+
var RECORD_KEYS = ["entries", "format"];
|
|
267
|
+
var ENTRY_KEYS = ["number", "phase", "shownAt", "version"];
|
|
268
|
+
function parseShownPlans(text) {
|
|
269
|
+
try {
|
|
270
|
+
if (typeof text !== "string")
|
|
271
|
+
return [];
|
|
272
|
+
const parsed = JSON.parse(text);
|
|
273
|
+
if (!isPlainObject(parsed) || !hasExactKeys(parsed, RECORD_KEYS))
|
|
274
|
+
return [];
|
|
275
|
+
if (parsed["format"] !== SHOWN_PLANS_FORMAT)
|
|
276
|
+
return [];
|
|
277
|
+
const rawEntries = parsed["entries"];
|
|
278
|
+
if (!Array.isArray(rawEntries))
|
|
279
|
+
return [];
|
|
280
|
+
const entries = [];
|
|
281
|
+
for (const raw of rawEntries) {
|
|
282
|
+
const entry = readEntry(raw);
|
|
283
|
+
if (entry === null)
|
|
284
|
+
return [];
|
|
285
|
+
entries.push(entry);
|
|
286
|
+
}
|
|
287
|
+
return hasDuplicates(entries) ? [] : entries;
|
|
288
|
+
} catch {
|
|
289
|
+
return [];
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
function serialiseShownPlans(entries) {
|
|
293
|
+
if (!Array.isArray(entries))
|
|
294
|
+
throw new Error(UNRECORDABLE);
|
|
295
|
+
const copied = [];
|
|
296
|
+
for (const raw of entries) {
|
|
297
|
+
const entry = readEntry(raw);
|
|
298
|
+
if (entry === null)
|
|
299
|
+
throw new Error(UNRECORDABLE);
|
|
300
|
+
copied.push(entry);
|
|
301
|
+
}
|
|
302
|
+
if (hasDuplicates(copied))
|
|
303
|
+
throw new Error(UNRECORDABLE);
|
|
304
|
+
return `${JSON.stringify({ format: SHOWN_PLANS_FORMAT, entries: copied }, null, 2)}
|
|
305
|
+
`;
|
|
306
|
+
}
|
|
307
|
+
var UNRECORDABLE = "the list just shown could not be recorded on this machine.";
|
|
308
|
+
function readEntry(raw) {
|
|
309
|
+
if (!isPlainObject(raw) || !hasExactKeys(raw, ENTRY_KEYS))
|
|
310
|
+
return null;
|
|
311
|
+
const phase = raw["phase"];
|
|
312
|
+
const number = raw["number"];
|
|
313
|
+
const version = raw["version"];
|
|
314
|
+
const shownAt = raw["shownAt"];
|
|
315
|
+
if (typeof phase !== "string" || phase === "")
|
|
316
|
+
return null;
|
|
317
|
+
if (!isPositiveInteger(number) || !isPositiveInteger(version))
|
|
318
|
+
return null;
|
|
319
|
+
if (!isIsoInstant(shownAt))
|
|
320
|
+
return null;
|
|
321
|
+
return { phase, number, version, shownAt };
|
|
322
|
+
}
|
|
323
|
+
function isPlainObject(value) {
|
|
324
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
325
|
+
return false;
|
|
326
|
+
const proto = Object.getPrototypeOf(value);
|
|
327
|
+
return proto === Object.prototype || proto === null;
|
|
328
|
+
}
|
|
329
|
+
function hasExactKeys(value, keys) {
|
|
330
|
+
const own = Object.keys(value).sort();
|
|
331
|
+
return own.length === keys.length && own.every((key, i) => key === keys[i]);
|
|
332
|
+
}
|
|
333
|
+
function isPositiveInteger(value) {
|
|
334
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 1;
|
|
335
|
+
}
|
|
336
|
+
function isIsoInstant(value) {
|
|
337
|
+
if (typeof value !== "string")
|
|
338
|
+
return false;
|
|
339
|
+
const ms = Date.parse(value);
|
|
340
|
+
return Number.isFinite(ms) && new Date(ms).toISOString() === value;
|
|
341
|
+
}
|
|
342
|
+
function hasDuplicates(entries) {
|
|
343
|
+
const phases = new Set(entries.map((entry) => entry.phase));
|
|
344
|
+
const numbers = new Set(entries.map((entry) => entry.number));
|
|
345
|
+
return phases.size !== entries.length || numbers.size !== entries.length;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// ../core/dist/phase-reference.js
|
|
349
|
+
var TYPED_NUMBER = /^(?:0|[1-9][0-9]*)$/;
|
|
350
|
+
function resolvePhaseReference(typed, phases) {
|
|
351
|
+
if (typeof typed !== "string" || typed === "")
|
|
352
|
+
return null;
|
|
353
|
+
const byIdentity = phases.filter((entry) => entry.phase === typed);
|
|
354
|
+
if (byIdentity.length > 0)
|
|
355
|
+
return byIdentity.length === 1 ? byIdentity[0] : null;
|
|
356
|
+
if (!TYPED_NUMBER.test(typed))
|
|
357
|
+
return null;
|
|
358
|
+
const byNumber = phases.filter((entry) => Number.isInteger(entry.number) && String(entry.number) === typed);
|
|
359
|
+
return byNumber.length === 1 ? byNumber[0] : null;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// ../core/dist/release-version.js
|
|
363
|
+
var RELEASE_VERSION_PATTERN = /^\d+\.\d+\.\d+$/;
|
|
364
|
+
function isReleaseVersion(value) {
|
|
365
|
+
return typeof value === "string" && RELEASE_VERSION_PATTERN.test(value);
|
|
366
|
+
}
|
|
367
|
+
function significant(part) {
|
|
368
|
+
const trimmed = part.replace(/^0+/, "");
|
|
369
|
+
return trimmed === "" ? "0" : trimmed;
|
|
370
|
+
}
|
|
371
|
+
function compareReleaseVersions(a, b) {
|
|
372
|
+
if (!isReleaseVersion(a) || !isReleaseVersion(b)) {
|
|
373
|
+
throw new TypeError("compareReleaseVersions takes two MAJOR.MINOR.PATCH versions");
|
|
374
|
+
}
|
|
375
|
+
const pa = a.split(".").map(significant);
|
|
376
|
+
const pb = b.split(".").map(significant);
|
|
377
|
+
for (let i = 0; i < 3; i++) {
|
|
378
|
+
const x = pa[i];
|
|
379
|
+
const y = pb[i];
|
|
380
|
+
if (x.length !== y.length)
|
|
381
|
+
return x.length - y.length;
|
|
382
|
+
if (x !== y)
|
|
383
|
+
return x < y ? -1 : 1;
|
|
384
|
+
}
|
|
385
|
+
return 0;
|
|
386
|
+
}
|
|
387
|
+
|
|
255
388
|
// ../events/dist/result.js
|
|
256
389
|
import { z as z2 } from "zod";
|
|
257
390
|
var firedGuardSchema = z2.object({
|
|
@@ -321,18 +454,19 @@ function hasLoneSurrogate(text) {
|
|
|
321
454
|
}
|
|
322
455
|
var LONE_SURROGATE_MESSAGE = "must be valid text: it holds half of a character pair with no other half";
|
|
323
456
|
|
|
324
|
-
// ../events/dist/build-record.js
|
|
325
|
-
import { z as z4 } from "zod";
|
|
326
|
-
|
|
327
457
|
// ../events/dist/state.js
|
|
328
458
|
import { z as z3 } from "zod";
|
|
329
459
|
var utcIso8601 = z3.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");
|
|
330
460
|
var STATE_RECORD_FORMAT = "halfcycle-state-record/v1";
|
|
331
461
|
var stepIdSchema = z3.string().regex(/^(l[0-5](-l[0-5])?|xl)\.[a-z0-9-]+$/);
|
|
332
|
-
var methodVersionSchema = z3.string().regex(
|
|
462
|
+
var methodVersionSchema = z3.string().regex(RELEASE_VERSION_PATTERN);
|
|
333
463
|
var anchorPathRegex = /^(?!\/)(?!.*#)(?!method\/)(?!(.*\/)?docs\/method\/).+$/;
|
|
334
464
|
var recordKindSchema = z3.enum(["artefact", "step-run", "gate", "intervention"]);
|
|
335
465
|
var methodLayerSchema = z3.enum(["l0", "l1", "l2", "l3", "l3-l4", "l4", "l5", "xl"]);
|
|
466
|
+
var PHASE_SCOPED_LAYERS = ["l4", "l5"];
|
|
467
|
+
function isPhaseScopedLayer(layer) {
|
|
468
|
+
return PHASE_SCOPED_LAYERS.includes(layer);
|
|
469
|
+
}
|
|
336
470
|
var evidenceKindSchema = z3.enum(["anchor", "diff", "repro", "question-answer", "no-op"]);
|
|
337
471
|
var declarableEvidenceKindSchema = z3.enum(["anchor", "diff", "repro", "question-answer"]);
|
|
338
472
|
var dispositionSchema = z3.enum([
|
|
@@ -506,6 +640,9 @@ var stateRecordSchema = z3.discriminatedUnion("recordKind", [
|
|
|
506
640
|
interventionRecordSchema
|
|
507
641
|
]);
|
|
508
642
|
|
|
643
|
+
// ../events/dist/build-record.js
|
|
644
|
+
import { z as z4 } from "zod";
|
|
645
|
+
|
|
509
646
|
// ../events/dist/control-characters.js
|
|
510
647
|
var CONTROL_CHARACTER_CLASS = "\\p{Cc}";
|
|
511
648
|
var CONTROL_CHARACTER = new RegExp(CONTROL_CHARACTER_CLASS, "u");
|
|
@@ -609,9 +746,11 @@ function statementText(max) {
|
|
|
609
746
|
});
|
|
610
747
|
}
|
|
611
748
|
var closeActorSchema = plainText(ACTOR_MAX);
|
|
749
|
+
var PHASE_NAME_RULE = `must be a phase name: not empty, no slash or backslash, not starting with a dot, no control characters, at most ${PHASE_NAME_MAX_BYTES} bytes`;
|
|
612
750
|
var phaseNameSchema = z4.string().refine((v) => isValidPhaseIdentity(v), {
|
|
613
|
-
message:
|
|
751
|
+
message: PHASE_NAME_RULE
|
|
614
752
|
});
|
|
753
|
+
var phaseIdentitySchema = z4.union([z4.string(), z4.number()], { error: "must be a phase name or a whole number" }).refine((v) => isValidPhaseIdentity(v), { message: PHASE_NAME_RULE });
|
|
615
754
|
var closeStatementsSchema = z4.object({
|
|
616
755
|
thesis: statementText(STATEMENT_MAX).nullable(),
|
|
617
756
|
outcome: statementText(STATEMENT_MAX).nullable(),
|
|
@@ -887,17 +1026,31 @@ var phaseEntryDecisionRecordSchema = z5.object({
|
|
|
887
1026
|
blockingFinding: z5.string().nullable(),
|
|
888
1027
|
decidedAt: utcIso86012
|
|
889
1028
|
}).strict();
|
|
1029
|
+
var methodUpdateSchema = z5.object({
|
|
1030
|
+
from: methodVersionSchema,
|
|
1031
|
+
to: methodVersionSchema
|
|
1032
|
+
}).strict();
|
|
890
1033
|
var openPhaseResponseSchema = z5.object({
|
|
891
1034
|
engagement: engagementStatusResponseSchema,
|
|
892
1035
|
/** What is open now. `null` is project scope. */
|
|
893
1036
|
phase: z5.string().nullable(),
|
|
894
1037
|
/** What was open before this decision. `null` is project scope. */
|
|
895
1038
|
previousPhase: z5.string().nullable(),
|
|
896
|
-
decision: phaseEntryDecisionRecordSchema
|
|
1039
|
+
decision: phaseEntryDecisionRecordSchema,
|
|
1040
|
+
/** The method version this project moved to with this open, or `null` when it did not move. */
|
|
1041
|
+
methodUpdated: methodUpdateSchema.nullable()
|
|
1042
|
+
}).strict();
|
|
1043
|
+
var acceptPhaseResponseSchema = engagementStatusResponseSchema.extend({
|
|
1044
|
+
methodUpdated: methodUpdateSchema.nullable()
|
|
897
1045
|
}).strict();
|
|
898
1046
|
|
|
899
1047
|
// ../events/dist/crew.js
|
|
900
1048
|
import { z as z6 } from "zod";
|
|
1049
|
+
|
|
1050
|
+
// ../events/dist/human-agent.js
|
|
1051
|
+
var HUMAN_AGENT = "Human";
|
|
1052
|
+
|
|
1053
|
+
// ../events/dist/crew.js
|
|
901
1054
|
var crewGroupSchema = z6.enum(["builder", "reviewer", "always-on"]);
|
|
902
1055
|
var crewTierSchema = z6.enum(["deep", "standard", "fast"]);
|
|
903
1056
|
var CREW_DISCIPLINE_MAX = 64;
|
|
@@ -1090,225 +1243,181 @@ var CREW_ROSTER = [
|
|
|
1090
1243
|
var CREW_ROSTER_SIZE = CREW_ROSTER.length;
|
|
1091
1244
|
var CREW_CALLSIGNS = CREW_ROSTER.map((member) => member.callsign);
|
|
1092
1245
|
|
|
1093
|
-
// ../events/dist/
|
|
1246
|
+
// ../events/dist/build-snapshot.js
|
|
1094
1247
|
import { z as z7 } from "zod";
|
|
1095
|
-
var
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
/** Confirm: the decision was recorded (approve or decline). 200. */
|
|
1102
|
-
RECORDED: "recorded",
|
|
1103
|
-
/** The human refused at the confirmation page. 400 on the poll. */
|
|
1104
|
-
DECLINED: "declined",
|
|
1105
|
-
/** The code's absolute expiry has passed. 400. */
|
|
1106
|
-
EXPIRED: "expired",
|
|
1107
|
-
/** Never minted, or already spent. 400. */
|
|
1108
|
-
UNKNOWN: "unknown",
|
|
1109
|
-
/** Confirm: neither `approve` nor `decline` was named. Nothing was bound. 400. */
|
|
1110
|
-
INVALID_DECISION: "invalid-decision",
|
|
1111
|
-
/** Confirm: an approval arrived with no signed-in identity. 400. */
|
|
1112
|
-
IDENTITY_REQUIRED: "identity-required",
|
|
1113
|
-
/** This deployment has no `SIGNIN_BASE_URL` / `DEVICE_AUTH_CONFIRM_SECRET`. 503. */
|
|
1114
|
-
NOT_CONFIGURED: "not-configured",
|
|
1115
|
-
/** Confirm: the shared confirmation secret was missing or wrong. 401. */
|
|
1116
|
-
UNAUTHORIZED: "unauthorized",
|
|
1117
|
-
/** Confirm: the code was already approved, declined or claimed. 409. */
|
|
1118
|
-
ALREADY_DECIDED: "already-decided",
|
|
1119
|
-
/**
|
|
1120
|
-
* Confirm: an approval named a signed-in identity whose account has not accepted
|
|
1121
|
-
* the terms of service and privacy policy currently in force. 400.
|
|
1122
|
-
*
|
|
1123
|
-
* NOT BOUND TO THE DEVICE CODE. Unlike every other refusal in this const, this one
|
|
1124
|
-
* says nothing about the code itself — the code is still `pending` after this
|
|
1125
|
-
* response, exactly as it was before the request, so the SAME userCode can be
|
|
1126
|
-
* confirmed again once the caller has recorded acceptance. `deviceConfirmRequestSchema`'s
|
|
1127
|
-
* `acceptTerms` is how a second confirm says it did.
|
|
1128
|
-
*/
|
|
1129
|
-
TERMS_REQUIRED: "terms-required"
|
|
1130
|
-
};
|
|
1131
|
-
var DEVICE_POLL_REFUSALS = [
|
|
1132
|
-
DEVICE_AUTH_STATUS.DECLINED,
|
|
1133
|
-
DEVICE_AUTH_STATUS.EXPIRED,
|
|
1134
|
-
DEVICE_AUTH_STATUS.UNKNOWN
|
|
1135
|
-
];
|
|
1136
|
-
var deviceAuthStartResponseSchema = z7.object({
|
|
1137
|
-
deviceCode: z7.string().min(1),
|
|
1138
|
-
userCode: z7.string().min(1),
|
|
1139
|
-
verificationUrl: z7.string().min(1),
|
|
1140
|
-
expiresAt: utcIso86013,
|
|
1141
|
-
pollIntervalMs: z7.number().int().positive()
|
|
1142
|
-
}).strict();
|
|
1143
|
-
var devicePollPendingSchema = z7.object({
|
|
1144
|
-
status: z7.literal(DEVICE_AUTH_STATUS.PENDING),
|
|
1145
|
-
pollIntervalMs: z7.number().int().positive()
|
|
1146
|
-
}).strict();
|
|
1147
|
-
var devicePollApprovedSchema = z7.object({
|
|
1148
|
-
status: z7.literal(DEVICE_AUTH_STATUS.APPROVED),
|
|
1149
|
-
accountId: z7.string().min(1),
|
|
1150
|
-
credential: z7.string().min(1),
|
|
1151
|
-
expiresAt: utcIso86013
|
|
1152
|
-
}).strict();
|
|
1153
|
-
var deviceAuthRefusalSchema = z7.object({
|
|
1154
|
-
status: z7.enum([
|
|
1155
|
-
DEVICE_AUTH_STATUS.DECLINED,
|
|
1156
|
-
DEVICE_AUTH_STATUS.EXPIRED,
|
|
1157
|
-
DEVICE_AUTH_STATUS.UNKNOWN,
|
|
1158
|
-
DEVICE_AUTH_STATUS.INVALID_DECISION,
|
|
1159
|
-
DEVICE_AUTH_STATUS.IDENTITY_REQUIRED,
|
|
1160
|
-
DEVICE_AUTH_STATUS.NOT_CONFIGURED,
|
|
1161
|
-
DEVICE_AUTH_STATUS.UNAUTHORIZED,
|
|
1162
|
-
DEVICE_AUTH_STATUS.ALREADY_DECIDED,
|
|
1163
|
-
DEVICE_AUTH_STATUS.TERMS_REQUIRED
|
|
1164
|
-
]),
|
|
1165
|
-
message: z7.string().min(1)
|
|
1166
|
-
}).strict();
|
|
1167
|
-
var devicePollResponseSchema = z7.union([
|
|
1168
|
-
devicePollPendingSchema,
|
|
1169
|
-
devicePollApprovedSchema,
|
|
1170
|
-
deviceAuthRefusalSchema
|
|
1171
|
-
]);
|
|
1172
|
-
var deviceConfirmRequestSchema = z7.object({
|
|
1173
|
-
userCode: z7.string().min(1),
|
|
1174
|
-
decision: z7.enum(["approve", "decline"]),
|
|
1175
|
-
externalAuthId: z7.string().min(1).optional(),
|
|
1176
|
-
email: z7.string().optional(),
|
|
1177
|
-
acceptTerms: z7.boolean().optional()
|
|
1178
|
-
}).strict();
|
|
1179
|
-
var deviceConfirmRecordedSchema = z7.object({
|
|
1180
|
-
status: z7.literal(DEVICE_AUTH_STATUS.RECORDED),
|
|
1181
|
-
accountId: z7.string().min(1).optional()
|
|
1182
|
-
}).strict();
|
|
1183
|
-
var deviceConfirmResponseSchema = z7.union([
|
|
1184
|
-
deviceConfirmRecordedSchema,
|
|
1185
|
-
deviceAuthRefusalSchema
|
|
1248
|
+
var buildTaskStateSchema = z7.enum([
|
|
1249
|
+
"pending",
|
|
1250
|
+
"in-flight",
|
|
1251
|
+
"completed",
|
|
1252
|
+
"failed",
|
|
1253
|
+
"blocked"
|
|
1186
1254
|
]);
|
|
1187
|
-
var
|
|
1188
|
-
var
|
|
1189
|
-
var
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1255
|
+
var BUILD_TASK_TITLE_MAX = 240;
|
|
1256
|
+
var BUILD_SNAPSHOT_TASK_LIMIT = 500;
|
|
1257
|
+
var buildTaskSchema = z7.object({
|
|
1258
|
+
// The task's own id, as the coordinator's graph names it.
|
|
1259
|
+
id: z7.string().min(1).max(64),
|
|
1260
|
+
// What the task is, in the coordinator's words. A description of the work,
|
|
1261
|
+
// bounded and scanned — never an excerpt of anybody's code.
|
|
1262
|
+
title: z7.string().min(1).max(BUILD_TASK_TITLE_MAX),
|
|
1263
|
+
// What the task is doing right now.
|
|
1264
|
+
state: buildTaskStateSchema,
|
|
1265
|
+
// Who holds it — a bare callsign, or the value naming a person when the work is
|
|
1266
|
+
// a person's. Absent when nothing has been dispatched to it yet.
|
|
1267
|
+
agent: z7.string().min(1).max(32).optional(),
|
|
1268
|
+
// The kind of work the holder does. Present exactly when an agent holds it.
|
|
1269
|
+
discipline: z7.string().min(1).max(64).optional(),
|
|
1270
|
+
// How much thinking the holder brings. Present exactly when an agent holds it,
|
|
1271
|
+
// and there is no field naming the supplier behind it.
|
|
1272
|
+
tier: z7.string().min(1).max(32).optional(),
|
|
1273
|
+
// The tasks this one cannot start before. Always present; empty means it waits
|
|
1274
|
+
// on nothing, which is different from not having been computed.
|
|
1275
|
+
dependsOn: z7.array(z7.string().min(1).max(64)).max(BUILD_SNAPSHOT_TASK_LIMIT),
|
|
1276
|
+
// The one task this task checks, when it is a check on other work. Absent on a
|
|
1277
|
+
// task that is doing the work rather than checking it.
|
|
1278
|
+
reviews: z7.string().min(1).max(64).optional(),
|
|
1279
|
+
// Which wave of the phase this task belongs to — the wave that writes the
|
|
1280
|
+
// specifications, or the wave that builds them. Absent when the graph does not
|
|
1281
|
+
// record it, which is different from the task belonging to neither.
|
|
1282
|
+
layer: methodLayerSchema.optional(),
|
|
1283
|
+
// When it was picked up, and when it settled. A reader computes how long it
|
|
1284
|
+
// took; neither this row nor this payload carries a duration.
|
|
1285
|
+
startedAt: utcIso8601.optional(),
|
|
1286
|
+
settledAt: utcIso8601.optional()
|
|
1287
|
+
}).strict().superRefine((task, ctx) => {
|
|
1288
|
+
if (task.reviews !== void 0 && task.reviews === task.id) {
|
|
1289
|
+
ctx.addIssue({
|
|
1290
|
+
code: z7.ZodIssueCode.custom,
|
|
1291
|
+
path: ["reviews"],
|
|
1292
|
+
message: `'${task.id}' reviews itself \u2014 a review names the OTHER task it checks.`
|
|
1293
|
+
});
|
|
1205
1294
|
}
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1295
|
+
if (task.layer !== void 0 && !isPhaseScopedLayer(task.layer)) {
|
|
1296
|
+
ctx.addIssue({
|
|
1297
|
+
code: z7.ZodIssueCode.custom,
|
|
1298
|
+
path: ["layer"],
|
|
1299
|
+
message: `'${task.layer}' is not a wave a phase runs \u2014 expected one of ${PHASE_SCOPED_LAYERS.join(", ")}.`
|
|
1300
|
+
});
|
|
1301
|
+
}
|
|
1302
|
+
const profiled = task.agent !== void 0 && task.agent !== HUMAN_AGENT;
|
|
1303
|
+
for (const field of ["discipline", "tier"]) {
|
|
1304
|
+
if (profiled && task[field] === void 0) {
|
|
1305
|
+
ctx.addIssue({
|
|
1306
|
+
code: z7.ZodIssueCode.custom,
|
|
1307
|
+
path: [field],
|
|
1308
|
+
message: `'${field}' is required on a task an agent holds.`
|
|
1309
|
+
});
|
|
1310
|
+
}
|
|
1311
|
+
if (!profiled && task[field] !== void 0) {
|
|
1312
|
+
ctx.addIssue({
|
|
1313
|
+
code: z7.ZodIssueCode.custom,
|
|
1314
|
+
path: [field],
|
|
1315
|
+
message: task.agent === void 0 ? `'${field}' must be absent on a task nobody holds.` : `'${field}' must be absent on work a person holds.`
|
|
1316
|
+
});
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
});
|
|
1320
|
+
var buildPhaseSchema = z7.string().min(1).max(128);
|
|
1321
|
+
var BUILD_PHASE_TITLE_MAX = 160;
|
|
1322
|
+
var buildPhaseTitleSchema = z7.string().min(1).max(BUILD_PHASE_TITLE_MAX);
|
|
1323
|
+
var buildTaskListSchema = z7.array(buildTaskSchema).max(BUILD_SNAPSHOT_TASK_LIMIT).superRefine((tasks, ctx) => {
|
|
1324
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1325
|
+
for (const [index, task] of tasks.entries()) {
|
|
1326
|
+
if (seen.has(task.id)) {
|
|
1327
|
+
ctx.addIssue({
|
|
1328
|
+
code: z7.ZodIssueCode.custom,
|
|
1329
|
+
path: [index, "id"],
|
|
1330
|
+
message: `duplicate task id '${task.id}' \u2014 one graph names each task once.`
|
|
1331
|
+
});
|
|
1332
|
+
}
|
|
1333
|
+
seen.add(task.id);
|
|
1334
|
+
}
|
|
1335
|
+
});
|
|
1336
|
+
var buildSnapshotSchema = z7.object({
|
|
1337
|
+
// Which phase this graph belongs to.
|
|
1338
|
+
phase: buildPhaseSchema,
|
|
1339
|
+
// That phase's name in words, when the project gives it one.
|
|
1340
|
+
phaseTitle: buildPhaseTitleSchema.optional(),
|
|
1341
|
+
// Every task on it. Never a partial graph and never a change since last time.
|
|
1342
|
+
tasks: buildTaskListSchema,
|
|
1343
|
+
// When this graph was last reported.
|
|
1344
|
+
reportedAt: utcIso8601,
|
|
1345
|
+
// When work on this phase was first reported with no approval of its scope in force.
|
|
1346
|
+
builtWithoutApproval: utcIso8601.optional()
|
|
1220
1347
|
}).strict();
|
|
1221
|
-
var
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
//
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
email: z9.string().min(1).optional()
|
|
1348
|
+
var buildPhaseEntrySchema = z7.object({
|
|
1349
|
+
// Which phase has a stored graph.
|
|
1350
|
+
phase: buildPhaseSchema,
|
|
1351
|
+
// That phase's name in words, when the project gave it one.
|
|
1352
|
+
phaseTitle: buildPhaseTitleSchema.optional(),
|
|
1353
|
+
// When that phase's graph was last reported.
|
|
1354
|
+
reportedAt: utcIso8601,
|
|
1355
|
+
// Present when work on this phase was reported with no approval of its scope in force.
|
|
1356
|
+
builtWithoutApproval: z7.literal(true).optional()
|
|
1231
1357
|
}).strict();
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
sessionToken: z10.string(),
|
|
1241
|
-
mcpUrl: z10.string(),
|
|
1242
|
-
guardUrl: z10.string(),
|
|
1243
|
-
controlTelemetryUrl: z10.string(),
|
|
1244
|
-
methodVersion: z10.string().optional(),
|
|
1245
|
-
consoleUrl: z10.string().optional()
|
|
1358
|
+
var buildSnapshotReportSchema = z7.object({
|
|
1359
|
+
// Which phase this graph belongs to.
|
|
1360
|
+
phase: buildPhaseSchema,
|
|
1361
|
+
// That phase's name in words, when the project gives it one.
|
|
1362
|
+
phaseTitle: buildPhaseTitleSchema.optional(),
|
|
1363
|
+
// The whole graph, every time. Reporting a change instead of the graph is the
|
|
1364
|
+
// one thing this route does not accept.
|
|
1365
|
+
tasks: buildTaskListSchema
|
|
1246
1366
|
}).strict();
|
|
1247
|
-
var
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1367
|
+
var buildSnapshotReportResultSchema = z7.object({
|
|
1368
|
+
// What happened. One outcome today; a refusal travels as the shared wire error.
|
|
1369
|
+
outcome: z7.literal("recorded"),
|
|
1370
|
+
// Which phase the graph was recorded against.
|
|
1371
|
+
phase: buildPhaseSchema,
|
|
1372
|
+
// When it was recorded.
|
|
1373
|
+
reportedAt: utcIso8601
|
|
1251
1374
|
}).strict();
|
|
1252
|
-
var NO_CREDENTIAL_PLACEHOLDER = "halfcycle:no-credential";
|
|
1253
|
-
var NOT_THIS_ACCOUNT_PLACEHOLDER = "halfcycle:not-this-account";
|
|
1254
|
-
var UPDATE_CLAUDE_CODE_PLACEHOLDER = "halfcycle:update-claude-code";
|
|
1255
|
-
var SWITCH_ACCOUNT_ANSWER = 'answer "s" when asked';
|
|
1256
|
-
var NO_CREDENTIAL_SENTENCE = 'Halfcycle is not connected: this machine has no working Halfcycle credential for this project. Run "npx halfcycle" in the project folder to sign in again.';
|
|
1257
|
-
var NOT_THIS_ACCOUNT_SENTENCE = `Halfcycle is not connected: this project belongs to another Halfcycle account. To switch, run "npx halfcycle" in this project and ${SWITCH_ACCOUNT_ANSWER}.`;
|
|
1258
|
-
var UPDATE_CLAUDE_CODE_SENTENCE = "Halfcycle is not connected: this Claude Code version does not name the server it connects to, so no credential was sent. Update Claude Code, then reopen.";
|
|
1259
|
-
var NO_NODE_SENTENCE = "Halfcycle is not connected: Node is not installed, or not on the path this session uses, so nothing here is checked. Install Node, then reopen.";
|
|
1260
|
-
var PLACEHOLDER_SENTENCES = Object.freeze({
|
|
1261
|
-
[NO_CREDENTIAL_PLACEHOLDER]: NO_CREDENTIAL_SENTENCE,
|
|
1262
|
-
[NOT_THIS_ACCOUNT_PLACEHOLDER]: NOT_THIS_ACCOUNT_SENTENCE,
|
|
1263
|
-
[UPDATE_CLAUDE_CODE_PLACEHOLDER]: UPDATE_CLAUDE_CODE_SENTENCE
|
|
1264
|
-
});
|
|
1265
1375
|
|
|
1266
|
-
// ../events/dist/
|
|
1267
|
-
import { z as
|
|
1268
|
-
var projectIdSchema = z11.string().uuid();
|
|
1269
|
-
var projectEngagementRequestSchema = z11.object({
|
|
1270
|
-
derivedName: z11.string()
|
|
1271
|
-
}).strict();
|
|
1376
|
+
// ../events/dist/phase-plan.js
|
|
1377
|
+
import { z as z9 } from "zod";
|
|
1272
1378
|
|
|
1273
|
-
// ../events/dist/
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
"
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
}
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1379
|
+
// ../events/dist/check-problems.js
|
|
1380
|
+
function isRecord(value) {
|
|
1381
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1382
|
+
}
|
|
1383
|
+
function dotted(path) {
|
|
1384
|
+
return path.map((key) => String(key)).join(".");
|
|
1385
|
+
}
|
|
1386
|
+
function isMissingKey(raw, path) {
|
|
1387
|
+
if (path.length === 0)
|
|
1388
|
+
return false;
|
|
1389
|
+
let parent = raw;
|
|
1390
|
+
for (const key of path.slice(0, -1)) {
|
|
1391
|
+
if (!isRecord(parent) && !Array.isArray(parent))
|
|
1392
|
+
return false;
|
|
1393
|
+
parent = parent[key];
|
|
1394
|
+
}
|
|
1395
|
+
return isRecord(parent) && !(String(path[path.length - 1]) in parent);
|
|
1396
|
+
}
|
|
1397
|
+
function shapeProblemsOf(schema, raw, undeclared) {
|
|
1398
|
+
const parsed = schema.safeParse(raw);
|
|
1399
|
+
if (parsed.success)
|
|
1400
|
+
return [];
|
|
1401
|
+
const problems = [];
|
|
1402
|
+
for (const issue of parsed.error.issues) {
|
|
1403
|
+
if (issue.code === "unrecognized_keys") {
|
|
1404
|
+
for (const key of issue.keys) {
|
|
1405
|
+
problems.push({ path: dotted([...issue.path, key]), message: undeclared });
|
|
1406
|
+
}
|
|
1407
|
+
} else if (isMissingKey(raw, issue.path)) {
|
|
1408
|
+
problems.push({ path: dotted(issue.path), message: "is required" });
|
|
1409
|
+
} else {
|
|
1410
|
+
problems.push({ path: dotted(issue.path), message: issue.message });
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
return problems;
|
|
1414
|
+
}
|
|
1415
|
+
function shown(value) {
|
|
1416
|
+
return looksLikeSecret(value) ? "a value that looks like a credential" : `"${value}"`;
|
|
1417
|
+
}
|
|
1309
1418
|
|
|
1310
1419
|
// ../events/dist/system-description.js
|
|
1311
|
-
import { z as
|
|
1420
|
+
import { z as z8 } from "zod";
|
|
1312
1421
|
var SYSTEM_DESCRIPTION_FORMAT = 1;
|
|
1313
1422
|
var SYSTEM_PARTS_MAX = 60;
|
|
1314
1423
|
var SYSTEM_CONNECTIONS_MAX = 400;
|
|
@@ -1320,11 +1429,11 @@ var SYSTEM_PART_DESCRIPTION_MAX = 160;
|
|
|
1320
1429
|
var SYSTEM_CONNECTION_LABEL_MAX = 60;
|
|
1321
1430
|
var SYSTEM_PART_ID_PATTERN = /^[a-z][a-z0-9-]{0,39}$/;
|
|
1322
1431
|
var SYSTEM_LANES = ["clients", "edge", "services", "data", "external"];
|
|
1323
|
-
var systemLaneSchema =
|
|
1432
|
+
var systemLaneSchema = z8.enum(SYSTEM_LANES, {
|
|
1324
1433
|
error: `must be one of ${SYSTEM_LANES.join(", ")}`
|
|
1325
1434
|
});
|
|
1326
1435
|
var SYSTEM_CONNECTION_KINDS = ["call", "event", "write", "external"];
|
|
1327
|
-
var systemConnectionKindSchema =
|
|
1436
|
+
var systemConnectionKindSchema = z8.enum(SYSTEM_CONNECTION_KINDS, {
|
|
1328
1437
|
error: `must be one of ${SYSTEM_CONNECTION_KINDS.join(", ")}`
|
|
1329
1438
|
});
|
|
1330
1439
|
var LINE_SEPARATOR = /[\u2028\u2029]/;
|
|
@@ -1333,7 +1442,7 @@ function descriptionText(max) {
|
|
|
1333
1442
|
message: "must be one line: it holds a line or paragraph separator"
|
|
1334
1443
|
});
|
|
1335
1444
|
}
|
|
1336
|
-
var systemPartIdSchema =
|
|
1445
|
+
var systemPartIdSchema = z8.string().regex(SYSTEM_PART_ID_PATTERN, {
|
|
1337
1446
|
message: "must be a short id: a lowercase letter, then lowercase letters, digits or hyphens, 40 characters at most"
|
|
1338
1447
|
}).refine((v) => !looksLikeSecret(v), { message: "looks like a password or key; choose another id" });
|
|
1339
1448
|
function hasEmptySegment(pattern) {
|
|
@@ -1349,74 +1458,39 @@ var SEGMENT_CHARACTERS = /^[A-Za-z0-9._\-*]*$/;
|
|
|
1349
1458
|
function hasForeignCharacter(pattern) {
|
|
1350
1459
|
return pattern.split("/").some((segment) => !SEGMENT_CHARACTERS.test(segment));
|
|
1351
1460
|
}
|
|
1352
|
-
var systemOwnedPatternSchema =
|
|
1461
|
+
var systemOwnedPatternSchema = z8.string().max(SYSTEM_OWNED_PATTERN_MAX, { message: `must be at most ${SYSTEM_OWNED_PATTERN_MAX} characters` }).refine((v) => !hasEmptySegment(v), {
|
|
1353
1462
|
message: "must not start or end with a slash, or have two slashes together"
|
|
1354
1463
|
}).refine((v) => !hasDotSegment(v), { message: 'must not have a "." or ".." folder in it' }).refine((v) => !hasPartialDoubleStar(v), { message: '"**" must stand alone between slashes' }).refine((v) => !hasForeignCharacter(v), {
|
|
1355
1464
|
message: 'may use only letters, digits, ".", "_", "-" and "*" between slashes'
|
|
1356
1465
|
}).refine((v) => !looksLikeSecret(v), {
|
|
1357
1466
|
message: "this path looks like a credential; list a shorter folder above it"
|
|
1358
1467
|
});
|
|
1359
|
-
var systemPartSchema =
|
|
1468
|
+
var systemPartSchema = z8.object({
|
|
1360
1469
|
id: systemPartIdSchema,
|
|
1361
1470
|
name: descriptionText(SYSTEM_PART_NAME_MAX),
|
|
1362
1471
|
description: descriptionText(SYSTEM_PART_DESCRIPTION_MAX),
|
|
1363
1472
|
lane: systemLaneSchema,
|
|
1364
|
-
slot:
|
|
1365
|
-
outside:
|
|
1366
|
-
owns:
|
|
1473
|
+
slot: z8.number({ error: `must be a whole number from 0 to ${SYSTEM_SLOT_MAX}` }).int({ error: `must be a whole number from 0 to ${SYSTEM_SLOT_MAX}` }).min(0, { error: `must be a whole number from 0 to ${SYSTEM_SLOT_MAX}` }).max(SYSTEM_SLOT_MAX, { error: `must be a whole number from 0 to ${SYSTEM_SLOT_MAX}` }),
|
|
1474
|
+
outside: z8.boolean(),
|
|
1475
|
+
owns: z8.array(systemOwnedPatternSchema).max(SYSTEM_OWNS_MAX, { message: `can list at most ${SYSTEM_OWNS_MAX} paths` })
|
|
1367
1476
|
}).strict();
|
|
1368
|
-
var systemConnectionSchema =
|
|
1477
|
+
var systemConnectionSchema = z8.object({
|
|
1369
1478
|
from: systemPartIdSchema,
|
|
1370
1479
|
to: systemPartIdSchema,
|
|
1371
1480
|
kind: systemConnectionKindSchema,
|
|
1372
1481
|
label: descriptionText(SYSTEM_CONNECTION_LABEL_MAX)
|
|
1373
1482
|
}).strict();
|
|
1374
|
-
var systemDescriptionSchema =
|
|
1375
|
-
format:
|
|
1376
|
-
parts:
|
|
1377
|
-
connections:
|
|
1483
|
+
var systemDescriptionSchema = z8.object({
|
|
1484
|
+
format: z8.literal(SYSTEM_DESCRIPTION_FORMAT, { error: `must be ${SYSTEM_DESCRIPTION_FORMAT}` }),
|
|
1485
|
+
parts: z8.array(systemPartSchema).min(1, { message: "must list at least one part" }).max(SYSTEM_PARTS_MAX, { message: `can hold at most ${SYSTEM_PARTS_MAX} parts (five lanes of twelve slots)` }),
|
|
1486
|
+
connections: z8.array(systemConnectionSchema).max(SYSTEM_CONNECTIONS_MAX, { message: `can hold at most ${SYSTEM_CONNECTIONS_MAX} connections` })
|
|
1378
1487
|
}).strict();
|
|
1379
|
-
var systemDescriptionProblemSchema =
|
|
1380
|
-
path:
|
|
1381
|
-
message:
|
|
1488
|
+
var systemDescriptionProblemSchema = z8.object({
|
|
1489
|
+
path: z8.string(),
|
|
1490
|
+
message: z8.string()
|
|
1382
1491
|
}).strict();
|
|
1383
|
-
function isRecord(value) {
|
|
1384
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1385
|
-
}
|
|
1386
|
-
function dotted(path) {
|
|
1387
|
-
return path.map((key) => String(key)).join(".");
|
|
1388
|
-
}
|
|
1389
|
-
function isMissingKey(raw, path) {
|
|
1390
|
-
if (path.length === 0)
|
|
1391
|
-
return false;
|
|
1392
|
-
let parent = raw;
|
|
1393
|
-
for (const key of path.slice(0, -1)) {
|
|
1394
|
-
if (!isRecord(parent) && !Array.isArray(parent))
|
|
1395
|
-
return false;
|
|
1396
|
-
parent = parent[key];
|
|
1397
|
-
}
|
|
1398
|
-
return isRecord(parent) && !(String(path[path.length - 1]) in parent);
|
|
1399
|
-
}
|
|
1400
1492
|
function shapeProblems(raw) {
|
|
1401
|
-
|
|
1402
|
-
if (parsed.success)
|
|
1403
|
-
return [];
|
|
1404
|
-
const problems = [];
|
|
1405
|
-
for (const issue of parsed.error.issues) {
|
|
1406
|
-
if (issue.code === "unrecognized_keys") {
|
|
1407
|
-
for (const key of issue.keys) {
|
|
1408
|
-
problems.push({ path: dotted([...issue.path, key]), message: "is not a field a description has" });
|
|
1409
|
-
}
|
|
1410
|
-
} else if (isMissingKey(raw, issue.path)) {
|
|
1411
|
-
problems.push({ path: dotted(issue.path), message: "is required" });
|
|
1412
|
-
} else {
|
|
1413
|
-
problems.push({ path: dotted(issue.path), message: issue.message });
|
|
1414
|
-
}
|
|
1415
|
-
}
|
|
1416
|
-
return problems;
|
|
1417
|
-
}
|
|
1418
|
-
function shown(value) {
|
|
1419
|
-
return looksLikeSecret(value) ? "a value that looks like a credential" : `"${value}"`;
|
|
1493
|
+
return shapeProblemsOf(systemDescriptionSchema, raw, "is not a field a description has");
|
|
1420
1494
|
}
|
|
1421
1495
|
function quoted(value) {
|
|
1422
1496
|
return typeof value === "string" ? shown(value) : "another part";
|
|
@@ -1573,19 +1647,19 @@ function ownerOf(path, description) {
|
|
|
1573
1647
|
}
|
|
1574
1648
|
return best === null ? null : best.id;
|
|
1575
1649
|
}
|
|
1576
|
-
var systemReportRecordedSchema =
|
|
1577
|
-
outcome:
|
|
1578
|
-
version:
|
|
1650
|
+
var systemReportRecordedSchema = z8.object({
|
|
1651
|
+
outcome: z8.literal("recorded"),
|
|
1652
|
+
version: z8.number().int().min(1),
|
|
1579
1653
|
reportedAt: utcIso8601
|
|
1580
1654
|
}).strict();
|
|
1581
|
-
var systemReportRefusedSchema =
|
|
1582
|
-
outcome:
|
|
1583
|
-
problems:
|
|
1655
|
+
var systemReportRefusedSchema = z8.object({
|
|
1656
|
+
outcome: z8.literal("refused"),
|
|
1657
|
+
problems: z8.array(systemDescriptionProblemSchema).min(1)
|
|
1584
1658
|
}).strict();
|
|
1585
|
-
var systemReportUnavailableSchema =
|
|
1586
|
-
outcome:
|
|
1659
|
+
var systemReportUnavailableSchema = z8.object({
|
|
1660
|
+
outcome: z8.literal("unavailable")
|
|
1587
1661
|
}).strict();
|
|
1588
|
-
var systemReportResultSchema =
|
|
1662
|
+
var systemReportResultSchema = z8.discriminatedUnion("outcome", [
|
|
1589
1663
|
systemReportRecordedSchema,
|
|
1590
1664
|
systemReportRefusedSchema,
|
|
1591
1665
|
systemReportUnavailableSchema
|
|
@@ -1594,10 +1668,19 @@ var SYSTEM_DRAW_SVG_MAX_BYTES = 2 * 1024 * 1024;
|
|
|
1594
1668
|
var SYSTEM_DRAW_MERMAID_MAX_BYTES = 256 * 1024;
|
|
1595
1669
|
var SYSTEM_DRAW_PNG_MAX_BYTES = 8 * 1024 * 1024;
|
|
1596
1670
|
var SYSTEM_DIAGRAM_MARKER_LINE = "<!-- drawn by Halfcycle from docs/system.json -->";
|
|
1597
|
-
var
|
|
1598
|
-
|
|
1599
|
-
|
|
1671
|
+
var systemDrawArchitectureRequestSchema = z8.object({
|
|
1672
|
+
picture: z8.literal("architecture"),
|
|
1673
|
+
version: z8.number().int().min(1)
|
|
1674
|
+
}).strict();
|
|
1675
|
+
var systemDrawPhaseRequestSchema = z8.object({
|
|
1676
|
+
picture: z8.literal("phase"),
|
|
1677
|
+
phase: phaseIdentitySchema,
|
|
1678
|
+
planVersion: z8.number().int().min(1)
|
|
1600
1679
|
}).strict();
|
|
1680
|
+
var systemDrawRequestSchema = z8.discriminatedUnion("picture", [
|
|
1681
|
+
systemDrawArchitectureRequestSchema,
|
|
1682
|
+
systemDrawPhaseRequestSchema
|
|
1683
|
+
]);
|
|
1601
1684
|
function utf8ByteLength2(text) {
|
|
1602
1685
|
let bytes = 0;
|
|
1603
1686
|
for (let i = 0; i < text.length; i++) {
|
|
@@ -1625,31 +1708,449 @@ function base64DecodedLength(text) {
|
|
|
1625
1708
|
return text.length / 4 * 3 - padding;
|
|
1626
1709
|
}
|
|
1627
1710
|
var SYSTEM_PICTURE_THEMES = ["broadsheet", "flightdeck"];
|
|
1628
|
-
var systemPictureSchema =
|
|
1629
|
-
svg:
|
|
1630
|
-
png:
|
|
1711
|
+
var systemPictureSchema = z8.object({
|
|
1712
|
+
svg: z8.string().min(1).refine((v) => utf8ByteLength2(v) <= SYSTEM_DRAW_SVG_MAX_BYTES, { message: "the picture is larger than 2 MB" }),
|
|
1713
|
+
png: z8.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" })
|
|
1631
1714
|
}).strict();
|
|
1632
|
-
var systemPicturesSchema =
|
|
1633
|
-
var systemDrawDrawnSchema =
|
|
1634
|
-
outcome:
|
|
1635
|
-
mermaid:
|
|
1715
|
+
var systemPicturesSchema = /* @__PURE__ */ z8.object(Object.fromEntries(SYSTEM_PICTURE_THEMES.map((theme) => [theme, systemPictureSchema]))).strict();
|
|
1716
|
+
var systemDrawDrawnSchema = /* @__PURE__ */ z8.object({
|
|
1717
|
+
outcome: z8.literal("drawn"),
|
|
1718
|
+
mermaid: z8.string().min(1).refine((v) => utf8ByteLength2(v) <= SYSTEM_DRAW_MERMAID_MAX_BYTES, {
|
|
1636
1719
|
message: "the diagram text is larger than 256 KB"
|
|
1637
1720
|
}),
|
|
1638
1721
|
pictures: systemPicturesSchema
|
|
1639
1722
|
}).strict();
|
|
1640
|
-
var systemDrawRefusedSchema =
|
|
1641
|
-
outcome:
|
|
1642
|
-
reason:
|
|
1723
|
+
var systemDrawRefusedSchema = z8.object({
|
|
1724
|
+
outcome: z8.literal("refused"),
|
|
1725
|
+
reason: z8.string().min(1)
|
|
1643
1726
|
}).strict();
|
|
1644
|
-
var systemDrawUnavailableSchema =
|
|
1645
|
-
outcome:
|
|
1727
|
+
var systemDrawUnavailableSchema = z8.object({
|
|
1728
|
+
outcome: z8.literal("unavailable")
|
|
1646
1729
|
}).strict();
|
|
1647
|
-
var systemDrawResultSchema =
|
|
1730
|
+
var systemDrawResultSchema = /* @__PURE__ */ z8.discriminatedUnion("outcome", [
|
|
1648
1731
|
systemDrawDrawnSchema,
|
|
1649
1732
|
systemDrawRefusedSchema,
|
|
1650
1733
|
systemDrawUnavailableSchema
|
|
1651
1734
|
]);
|
|
1652
1735
|
|
|
1736
|
+
// ../events/dist/phase-plan.js
|
|
1737
|
+
var PHASE_PLAN_FORMAT = 1;
|
|
1738
|
+
var PHASE_PLAN_CHANGES_MAX = 60;
|
|
1739
|
+
var PHASE_PLAN_SUMMARY_MAX = 160;
|
|
1740
|
+
var SCOPE_NOTE_MAX = 500;
|
|
1741
|
+
var PHASE_PLAN_PHASE_MAX = buildPhaseSchema.maxLength ?? 0;
|
|
1742
|
+
var PHASE_PLAN_CHANGE_KINDS = ["add", "change", "remove"];
|
|
1743
|
+
var phasePlanChangeSchema = z9.enum(PHASE_PLAN_CHANGE_KINDS, {
|
|
1744
|
+
error: `must be one of ${PHASE_PLAN_CHANGE_KINDS.join(", ")}`
|
|
1745
|
+
});
|
|
1746
|
+
var phasePlanPhaseSchema = phaseIdentitySchema.refine((v) => !isValidPhaseIdentity(v) || buildPhaseSchema.safeParse(String(v)).success, { message: `must be at most ${PHASE_PLAN_PHASE_MAX} characters` });
|
|
1747
|
+
var phasePlanSummarySchema = descriptionText(PHASE_PLAN_SUMMARY_MAX);
|
|
1748
|
+
var phasePlanEntrySchema = z9.object({
|
|
1749
|
+
part: systemPartIdSchema,
|
|
1750
|
+
change: phasePlanChangeSchema,
|
|
1751
|
+
summary: phasePlanSummarySchema
|
|
1752
|
+
}).strict();
|
|
1753
|
+
var phasePlanSchema = z9.object({
|
|
1754
|
+
format: z9.literal(PHASE_PLAN_FORMAT, { error: `must be ${PHASE_PLAN_FORMAT}` }),
|
|
1755
|
+
phase: phasePlanPhaseSchema,
|
|
1756
|
+
changes: z9.array(phasePlanEntrySchema).min(1, { message: "must list at least one part" }).max(PHASE_PLAN_CHANGES_MAX, { message: `can list at most ${PHASE_PLAN_CHANGES_MAX} parts` })
|
|
1757
|
+
}).strict();
|
|
1758
|
+
function planCrossFieldProblems(raw, description) {
|
|
1759
|
+
const problems = [];
|
|
1760
|
+
if (!isRecord(raw) || !Array.isArray(raw.changes))
|
|
1761
|
+
return problems;
|
|
1762
|
+
const known = new Set(description.parts.map((p) => p.id));
|
|
1763
|
+
const named = /* @__PURE__ */ new Map();
|
|
1764
|
+
raw.changes.forEach((change, i) => {
|
|
1765
|
+
if (!isRecord(change) || typeof change.part !== "string")
|
|
1766
|
+
return;
|
|
1767
|
+
if (!known.has(change.part)) {
|
|
1768
|
+
problems.push({ path: `changes.${i}.part`, message: `names no part in the system description: ${shown(change.part)}` });
|
|
1769
|
+
}
|
|
1770
|
+
const earlier = named.get(change.part);
|
|
1771
|
+
if (earlier === void 0) {
|
|
1772
|
+
named.set(change.part, i);
|
|
1773
|
+
} else {
|
|
1774
|
+
problems.push({ path: `changes.${i}.part`, message: `${shown(change.part)} is already listed at changes.${earlier}` });
|
|
1775
|
+
}
|
|
1776
|
+
});
|
|
1777
|
+
return problems;
|
|
1778
|
+
}
|
|
1779
|
+
function checkPhasePlan(value, description) {
|
|
1780
|
+
const problems = [
|
|
1781
|
+
...shapeProblemsOf(phasePlanSchema, value, "is not a field a plan has"),
|
|
1782
|
+
...planCrossFieldProblems(value, description)
|
|
1783
|
+
];
|
|
1784
|
+
if (problems.length > 0)
|
|
1785
|
+
return { ok: false, problems };
|
|
1786
|
+
return { ok: true, plan: phasePlanSchema.parse(value) };
|
|
1787
|
+
}
|
|
1788
|
+
var scopePhaseSchema = buildPhaseSchema.refine((v) => isValidPhaseIdentity(v), {
|
|
1789
|
+
message: "must be a phase name"
|
|
1790
|
+
});
|
|
1791
|
+
var SCOPE_STATE_NAMES = [
|
|
1792
|
+
"not-planned",
|
|
1793
|
+
"waiting",
|
|
1794
|
+
"sent-back",
|
|
1795
|
+
"approved",
|
|
1796
|
+
"partly-stale",
|
|
1797
|
+
"withdrawn"
|
|
1798
|
+
];
|
|
1799
|
+
var scopeStateNameSchema = z9.enum(SCOPE_STATE_NAMES);
|
|
1800
|
+
var plannedScopeStateSchema = scopeStateNameSchema.exclude(["not-planned"]);
|
|
1801
|
+
var SCOPE_PART_STATUSES = ["approved", "waiting"];
|
|
1802
|
+
var scopePartStatusSchema = z9.enum(SCOPE_PART_STATUSES);
|
|
1803
|
+
var SCOPE_ACTS = ["approve", "send-back", "withdraw"];
|
|
1804
|
+
var scopeActSchema = z9.enum(SCOPE_ACTS);
|
|
1805
|
+
var SCOPE_GIVEN_IN = ["console", "chat"];
|
|
1806
|
+
var scopeGivenInSchema = z9.enum(SCOPE_GIVEN_IN);
|
|
1807
|
+
var scopeNoteSchema = descriptionText(SCOPE_NOTE_MAX);
|
|
1808
|
+
var scopeNoteWireSchema = z9.string().max(SCOPE_NOTE_MAX);
|
|
1809
|
+
var SCOPE_PART_VERDICTS = ["ok", "change"];
|
|
1810
|
+
var scopePartVerdictSchema = z9.enum(SCOPE_PART_VERDICTS);
|
|
1811
|
+
var scopeDecisionPartSchema = z9.object({
|
|
1812
|
+
part: systemPartIdSchema,
|
|
1813
|
+
verdict: scopePartVerdictSchema,
|
|
1814
|
+
note: scopeNoteWireSchema.optional()
|
|
1815
|
+
}).strict();
|
|
1816
|
+
var planVersionSchema = z9.number().int().min(1);
|
|
1817
|
+
var phasePlanRequestSchema = z9.object({
|
|
1818
|
+
descriptionVersion: z9.number().int().min(1),
|
|
1819
|
+
plan: z9.unknown()
|
|
1820
|
+
}).strict();
|
|
1821
|
+
var phasePlanRecordedSchema = z9.object({
|
|
1822
|
+
outcome: z9.literal("recorded"),
|
|
1823
|
+
version: planVersionSchema,
|
|
1824
|
+
phaseNumber: z9.number().int().min(1),
|
|
1825
|
+
state: plannedScopeStateSchema,
|
|
1826
|
+
changedParts: z9.array(systemPartIdSchema).max(PHASE_PLAN_CHANGES_MAX),
|
|
1827
|
+
typedApprovalOffered: z9.boolean(),
|
|
1828
|
+
reportedAt: utcIso8601
|
|
1829
|
+
}).strict();
|
|
1830
|
+
var phasePlanRefusedSchema = z9.object({
|
|
1831
|
+
outcome: z9.literal("refused"),
|
|
1832
|
+
problems: z9.array(systemDescriptionProblemSchema).min(1)
|
|
1833
|
+
}).strict();
|
|
1834
|
+
var phasePlanUnavailableSchema = z9.object({
|
|
1835
|
+
outcome: z9.literal("unavailable")
|
|
1836
|
+
}).strict();
|
|
1837
|
+
var phasePlanResultSchema = z9.discriminatedUnion("outcome", [
|
|
1838
|
+
phasePlanRecordedSchema,
|
|
1839
|
+
phasePlanRefusedSchema,
|
|
1840
|
+
phasePlanUnavailableSchema
|
|
1841
|
+
]);
|
|
1842
|
+
var scopeApproveRequestSchema = z9.object({
|
|
1843
|
+
act: z9.literal("approve"),
|
|
1844
|
+
planVersion: planVersionSchema,
|
|
1845
|
+
parts: z9.array(scopeDecisionPartSchema).max(PHASE_PLAN_CHANGES_MAX)
|
|
1846
|
+
}).strict();
|
|
1847
|
+
var scopeSendBackRequestSchema = z9.object({
|
|
1848
|
+
act: z9.literal("send-back"),
|
|
1849
|
+
planVersion: planVersionSchema,
|
|
1850
|
+
parts: z9.array(scopeDecisionPartSchema).max(PHASE_PLAN_CHANGES_MAX),
|
|
1851
|
+
note: scopeNoteWireSchema.optional()
|
|
1852
|
+
}).strict();
|
|
1853
|
+
var scopeWithdrawRequestSchema = z9.object({
|
|
1854
|
+
act: z9.literal("withdraw"),
|
|
1855
|
+
planVersion: planVersionSchema
|
|
1856
|
+
}).strict();
|
|
1857
|
+
var scopeDecisionRequestSchema = z9.discriminatedUnion("act", [
|
|
1858
|
+
scopeApproveRequestSchema,
|
|
1859
|
+
scopeSendBackRequestSchema,
|
|
1860
|
+
scopeWithdrawRequestSchema
|
|
1861
|
+
]);
|
|
1862
|
+
var scopeDecisionRecordedSchema = z9.object({
|
|
1863
|
+
outcome: z9.literal("recorded"),
|
|
1864
|
+
act: scopeActSchema,
|
|
1865
|
+
phase: scopePhaseSchema,
|
|
1866
|
+
planVersion: planVersionSchema,
|
|
1867
|
+
state: plannedScopeStateSchema,
|
|
1868
|
+
decidedAt: utcIso8601
|
|
1869
|
+
}).strict();
|
|
1870
|
+
var scopeDecisionAlreadyApprovedSchema = z9.object({
|
|
1871
|
+
outcome: z9.literal("already-approved"),
|
|
1872
|
+
replanNeeded: z9.boolean(),
|
|
1873
|
+
reason: z9.string().min(1)
|
|
1874
|
+
}).strict();
|
|
1875
|
+
var scopeDecisionRefusedSchema = z9.object({
|
|
1876
|
+
outcome: z9.literal("refused"),
|
|
1877
|
+
problems: z9.array(systemDescriptionProblemSchema).min(1)
|
|
1878
|
+
}).strict();
|
|
1879
|
+
var scopeDecisionUnavailableSchema = z9.object({
|
|
1880
|
+
outcome: z9.literal("unavailable")
|
|
1881
|
+
}).strict();
|
|
1882
|
+
function reasonOutcome(outcome) {
|
|
1883
|
+
return z9.object({ outcome: z9.literal(outcome), reason: z9.string().min(1) }).strict();
|
|
1884
|
+
}
|
|
1885
|
+
var scopeDecisionResultSchema = z9.discriminatedUnion("outcome", [
|
|
1886
|
+
scopeDecisionRecordedSchema,
|
|
1887
|
+
scopeDecisionAlreadyApprovedSchema,
|
|
1888
|
+
scopeDecisionRefusedSchema,
|
|
1889
|
+
scopeDecisionUnavailableSchema,
|
|
1890
|
+
reasonOutcome("not-owner"),
|
|
1891
|
+
reasonOutcome("no-owner"),
|
|
1892
|
+
reasonOutcome("plan-changed"),
|
|
1893
|
+
reasonOutcome("nothing-to-withdraw"),
|
|
1894
|
+
reasonOutcome("build-started")
|
|
1895
|
+
]);
|
|
1896
|
+
var scopeReadPartSchema = z9.object({
|
|
1897
|
+
part: systemPartIdSchema,
|
|
1898
|
+
name: systemPartSchema.shape.name,
|
|
1899
|
+
change: phasePlanChangeSchema,
|
|
1900
|
+
summary: phasePlanSummarySchema,
|
|
1901
|
+
status: scopePartStatusSchema,
|
|
1902
|
+
changed: z9.boolean()
|
|
1903
|
+
}).strict();
|
|
1904
|
+
var scopeReadDecisionSchema = z9.object({
|
|
1905
|
+
act: scopeActSchema,
|
|
1906
|
+
planVersion: planVersionSchema,
|
|
1907
|
+
givenIn: scopeGivenInSchema,
|
|
1908
|
+
at: utcIso8601,
|
|
1909
|
+
parts: z9.array(scopeDecisionPartSchema).max(PHASE_PLAN_CHANGES_MAX),
|
|
1910
|
+
note: scopeNoteWireSchema.optional()
|
|
1911
|
+
}).strict();
|
|
1912
|
+
var scopeReadNotPlannedSchema = z9.object({
|
|
1913
|
+
phase: scopePhaseSchema,
|
|
1914
|
+
state: z9.literal("not-planned"),
|
|
1915
|
+
canDecide: z9.boolean()
|
|
1916
|
+
}).strict();
|
|
1917
|
+
var scopeReadPlannedSchema = z9.object({
|
|
1918
|
+
phase: scopePhaseSchema,
|
|
1919
|
+
state: plannedScopeStateSchema,
|
|
1920
|
+
phaseNumber: z9.number().int().min(1),
|
|
1921
|
+
planVersion: planVersionSchema,
|
|
1922
|
+
parts: z9.array(scopeReadPartSchema).min(1).max(PHASE_PLAN_CHANGES_MAX),
|
|
1923
|
+
decisions: z9.array(scopeReadDecisionSchema),
|
|
1924
|
+
canDecide: z9.boolean(),
|
|
1925
|
+
replanLine: z9.string().min(1).optional()
|
|
1926
|
+
}).strict();
|
|
1927
|
+
var scopeReadSchema = z9.union([scopeReadNotPlannedSchema, scopeReadPlannedSchema]);
|
|
1928
|
+
|
|
1929
|
+
// ../events/dist/client-version.js
|
|
1930
|
+
var CHAT_APPROVAL_CLIENT_VERSION = "0.4.7";
|
|
1931
|
+
function clientMeetsChatApprovalFloor(version) {
|
|
1932
|
+
return isReleaseVersion(version) && compareReleaseVersions(version, CHAT_APPROVAL_CLIENT_VERSION) >= 0;
|
|
1933
|
+
}
|
|
1934
|
+
var TYPED_APPROVAL_OFF_SENTENCE = "Approving by typing is switched off for now.";
|
|
1935
|
+
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.`;
|
|
1936
|
+
|
|
1937
|
+
// ../events/dist/device-auth.js
|
|
1938
|
+
import { z as z10 } from "zod";
|
|
1939
|
+
var utcIso86013 = z10.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");
|
|
1940
|
+
var DEVICE_AUTH_STATUS = {
|
|
1941
|
+
/** The poll: minted, nobody has acted yet. Keep polling. 200. */
|
|
1942
|
+
PENDING: "pending",
|
|
1943
|
+
/** The poll: approved, and the credential is in this body. Once only. 200. */
|
|
1944
|
+
APPROVED: "approved",
|
|
1945
|
+
/** Confirm: the decision was recorded (approve or decline). 200. */
|
|
1946
|
+
RECORDED: "recorded",
|
|
1947
|
+
/** The human refused at the confirmation page. 400 on the poll. */
|
|
1948
|
+
DECLINED: "declined",
|
|
1949
|
+
/** The code's absolute expiry has passed. 400. */
|
|
1950
|
+
EXPIRED: "expired",
|
|
1951
|
+
/** Never minted, or already spent. 400. */
|
|
1952
|
+
UNKNOWN: "unknown",
|
|
1953
|
+
/** Confirm: neither `approve` nor `decline` was named. Nothing was bound. 400. */
|
|
1954
|
+
INVALID_DECISION: "invalid-decision",
|
|
1955
|
+
/** Confirm: an approval arrived with no signed-in identity. 400. */
|
|
1956
|
+
IDENTITY_REQUIRED: "identity-required",
|
|
1957
|
+
/** This deployment has no `SIGNIN_BASE_URL` / `DEVICE_AUTH_CONFIRM_SECRET`. 503. */
|
|
1958
|
+
NOT_CONFIGURED: "not-configured",
|
|
1959
|
+
/** Confirm: the shared confirmation secret was missing or wrong. 401. */
|
|
1960
|
+
UNAUTHORIZED: "unauthorized",
|
|
1961
|
+
/** Confirm: the code was already approved, declined or claimed. 409. */
|
|
1962
|
+
ALREADY_DECIDED: "already-decided",
|
|
1963
|
+
/**
|
|
1964
|
+
* Confirm: an approval named a signed-in identity whose account has not accepted
|
|
1965
|
+
* the terms of service and privacy policy currently in force. 400.
|
|
1966
|
+
*
|
|
1967
|
+
* NOT BOUND TO THE DEVICE CODE. Unlike every other refusal in this const, this one
|
|
1968
|
+
* says nothing about the code itself — the code is still `pending` after this
|
|
1969
|
+
* response, exactly as it was before the request, so the SAME userCode can be
|
|
1970
|
+
* confirmed again once the caller has recorded acceptance. `deviceConfirmRequestSchema`'s
|
|
1971
|
+
* `acceptTerms` is how a second confirm says it did.
|
|
1972
|
+
*/
|
|
1973
|
+
TERMS_REQUIRED: "terms-required"
|
|
1974
|
+
};
|
|
1975
|
+
var DEVICE_POLL_REFUSALS = [
|
|
1976
|
+
DEVICE_AUTH_STATUS.DECLINED,
|
|
1977
|
+
DEVICE_AUTH_STATUS.EXPIRED,
|
|
1978
|
+
DEVICE_AUTH_STATUS.UNKNOWN
|
|
1979
|
+
];
|
|
1980
|
+
var deviceAuthStartResponseSchema = z10.object({
|
|
1981
|
+
deviceCode: z10.string().min(1),
|
|
1982
|
+
userCode: z10.string().min(1),
|
|
1983
|
+
verificationUrl: z10.string().min(1),
|
|
1984
|
+
expiresAt: utcIso86013,
|
|
1985
|
+
pollIntervalMs: z10.number().int().positive()
|
|
1986
|
+
}).strict();
|
|
1987
|
+
var devicePollPendingSchema = z10.object({
|
|
1988
|
+
status: z10.literal(DEVICE_AUTH_STATUS.PENDING),
|
|
1989
|
+
pollIntervalMs: z10.number().int().positive()
|
|
1990
|
+
}).strict();
|
|
1991
|
+
var devicePollApprovedSchema = z10.object({
|
|
1992
|
+
status: z10.literal(DEVICE_AUTH_STATUS.APPROVED),
|
|
1993
|
+
accountId: z10.string().min(1),
|
|
1994
|
+
credential: z10.string().min(1),
|
|
1995
|
+
expiresAt: utcIso86013
|
|
1996
|
+
}).strict();
|
|
1997
|
+
var deviceAuthRefusalSchema = z10.object({
|
|
1998
|
+
status: z10.enum([
|
|
1999
|
+
DEVICE_AUTH_STATUS.DECLINED,
|
|
2000
|
+
DEVICE_AUTH_STATUS.EXPIRED,
|
|
2001
|
+
DEVICE_AUTH_STATUS.UNKNOWN,
|
|
2002
|
+
DEVICE_AUTH_STATUS.INVALID_DECISION,
|
|
2003
|
+
DEVICE_AUTH_STATUS.IDENTITY_REQUIRED,
|
|
2004
|
+
DEVICE_AUTH_STATUS.NOT_CONFIGURED,
|
|
2005
|
+
DEVICE_AUTH_STATUS.UNAUTHORIZED,
|
|
2006
|
+
DEVICE_AUTH_STATUS.ALREADY_DECIDED,
|
|
2007
|
+
DEVICE_AUTH_STATUS.TERMS_REQUIRED
|
|
2008
|
+
]),
|
|
2009
|
+
message: z10.string().min(1)
|
|
2010
|
+
}).strict();
|
|
2011
|
+
var devicePollResponseSchema = z10.union([
|
|
2012
|
+
devicePollPendingSchema,
|
|
2013
|
+
devicePollApprovedSchema,
|
|
2014
|
+
deviceAuthRefusalSchema
|
|
2015
|
+
]);
|
|
2016
|
+
var deviceConfirmRequestSchema = z10.object({
|
|
2017
|
+
userCode: z10.string().min(1),
|
|
2018
|
+
decision: z10.enum(["approve", "decline"]),
|
|
2019
|
+
externalAuthId: z10.string().min(1).optional(),
|
|
2020
|
+
email: z10.string().optional(),
|
|
2021
|
+
acceptTerms: z10.boolean().optional()
|
|
2022
|
+
}).strict();
|
|
2023
|
+
var deviceConfirmRecordedSchema = z10.object({
|
|
2024
|
+
status: z10.literal(DEVICE_AUTH_STATUS.RECORDED),
|
|
2025
|
+
accountId: z10.string().min(1).optional()
|
|
2026
|
+
}).strict();
|
|
2027
|
+
var deviceConfirmResponseSchema = z10.union([
|
|
2028
|
+
deviceConfirmRecordedSchema,
|
|
2029
|
+
deviceAuthRefusalSchema
|
|
2030
|
+
]);
|
|
2031
|
+
var LOOPBACK_REDIRECT_HOST = "127.0.0.1";
|
|
2032
|
+
var LOOPBACK_PORT_MIN = 1024;
|
|
2033
|
+
var LOOPBACK_PORT_MAX = 65535;
|
|
2034
|
+
var LOOPBACK_CALLBACK_PATH = "/halfcycle-cli-callback";
|
|
2035
|
+
var LOOPBACK_QUERY_PORT = "loopback_port";
|
|
2036
|
+
var LOOPBACK_QUERY_STATE = "loopback_state";
|
|
2037
|
+
var LOOPBACK_QUERY_RESULT = "result";
|
|
2038
|
+
var LOOPBACK_RESULT = {
|
|
2039
|
+
APPROVED: "approved",
|
|
2040
|
+
DECLINED: "declined"
|
|
2041
|
+
};
|
|
2042
|
+
var DASHBOARD_URL = "https://app.halfcycle.ai";
|
|
2043
|
+
function loopbackVerificationUrl(verificationUrl, target) {
|
|
2044
|
+
let url;
|
|
2045
|
+
try {
|
|
2046
|
+
url = new URL(verificationUrl);
|
|
2047
|
+
} catch {
|
|
2048
|
+
return verificationUrl;
|
|
2049
|
+
}
|
|
2050
|
+
url.searchParams.set(LOOPBACK_QUERY_PORT, String(target.port));
|
|
2051
|
+
url.searchParams.set(LOOPBACK_QUERY_STATE, target.state);
|
|
2052
|
+
return url.toString();
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
// ../events/dist/join-refusal.js
|
|
2056
|
+
import { z as z11 } from "zod";
|
|
2057
|
+
var NOT_THIS_ACCOUNT_REASON = "not-this-account";
|
|
2058
|
+
var JOIN_REFUSAL_STATUS = 403;
|
|
2059
|
+
var notThisAccountRefusalSchema = z11.object({
|
|
2060
|
+
statusCode: z11.literal(JOIN_REFUSAL_STATUS),
|
|
2061
|
+
error: z11.string(),
|
|
2062
|
+
message: z11.string(),
|
|
2063
|
+
reason: z11.literal(NOT_THIS_ACCOUNT_REASON)
|
|
2064
|
+
}).strict();
|
|
2065
|
+
var joinRefusalSchema = z11.union([notThisAccountRefusalSchema, wireErrorSchema]);
|
|
2066
|
+
function isNotThisAccountRefusal(body) {
|
|
2067
|
+
return typeof body === "object" && body !== null && body.reason === NOT_THIS_ACCOUNT_REASON;
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2070
|
+
// ../events/dist/account-identity.js
|
|
2071
|
+
import { z as z12 } from "zod";
|
|
2072
|
+
var accountIdentitySchema = z12.object({
|
|
2073
|
+
accountId: z12.string().min(1),
|
|
2074
|
+
email: z12.string().min(1).optional()
|
|
2075
|
+
}).strict();
|
|
2076
|
+
function safeParseAccountIdentity(payload) {
|
|
2077
|
+
return accountIdentitySchema.safeParse(payload);
|
|
2078
|
+
}
|
|
2079
|
+
|
|
2080
|
+
// ../events/dist/credential-wire.js
|
|
2081
|
+
import { z as z13 } from "zod";
|
|
2082
|
+
var engagementCredentialResponseSchema = z13.object({
|
|
2083
|
+
engagementId: z13.string(),
|
|
2084
|
+
sessionToken: z13.string(),
|
|
2085
|
+
mcpUrl: z13.string(),
|
|
2086
|
+
guardUrl: z13.string(),
|
|
2087
|
+
controlTelemetryUrl: z13.string(),
|
|
2088
|
+
methodVersion: z13.string().optional(),
|
|
2089
|
+
consoleUrl: z13.string().optional()
|
|
2090
|
+
}).strict();
|
|
2091
|
+
var ciTokenExchangeResponseSchema = z13.object({
|
|
2092
|
+
token: z13.string(),
|
|
2093
|
+
expiresAt: utcIso8601,
|
|
2094
|
+
engagementId: z13.string()
|
|
2095
|
+
}).strict();
|
|
2096
|
+
var NO_CREDENTIAL_PLACEHOLDER = "halfcycle:no-credential";
|
|
2097
|
+
var NOT_THIS_ACCOUNT_PLACEHOLDER = "halfcycle:not-this-account";
|
|
2098
|
+
var UPDATE_CLAUDE_CODE_PLACEHOLDER = "halfcycle:update-claude-code";
|
|
2099
|
+
var SWITCH_ACCOUNT_ANSWER = 'answer "s" when asked';
|
|
2100
|
+
var NO_CREDENTIAL_SENTENCE = 'Halfcycle is not connected: this machine has no working Halfcycle credential for this project. Run "npx halfcycle" in the project folder to sign in again.';
|
|
2101
|
+
var NOT_THIS_ACCOUNT_SENTENCE = `Halfcycle is not connected: this project belongs to another Halfcycle account. To switch, run "npx halfcycle" in this project and ${SWITCH_ACCOUNT_ANSWER}.`;
|
|
2102
|
+
var UPDATE_CLAUDE_CODE_SENTENCE = "Halfcycle is not connected: this Claude Code version does not name the server it connects to, so no credential was sent. Update Claude Code, then reopen.";
|
|
2103
|
+
var NO_NODE_SENTENCE = "Halfcycle is not connected: Node is not installed, or not on the path this session uses, so nothing here is checked. Install Node, then reopen.";
|
|
2104
|
+
var PLACEHOLDER_SENTENCES = Object.freeze({
|
|
2105
|
+
[NO_CREDENTIAL_PLACEHOLDER]: NO_CREDENTIAL_SENTENCE,
|
|
2106
|
+
[NOT_THIS_ACCOUNT_PLACEHOLDER]: NOT_THIS_ACCOUNT_SENTENCE,
|
|
2107
|
+
[UPDATE_CLAUDE_CODE_PLACEHOLDER]: UPDATE_CLAUDE_CODE_SENTENCE
|
|
2108
|
+
});
|
|
2109
|
+
|
|
2110
|
+
// ../events/dist/project-engagement.js
|
|
2111
|
+
import { z as z14 } from "zod";
|
|
2112
|
+
var projectIdSchema = z14.string().uuid();
|
|
2113
|
+
var projectEngagementRequestSchema = z14.object({
|
|
2114
|
+
derivedName: z14.string()
|
|
2115
|
+
}).strict();
|
|
2116
|
+
|
|
2117
|
+
// ../events/dist/phase-record.js
|
|
2118
|
+
import { z as z15 } from "zod";
|
|
2119
|
+
var phaseRecordRefusalReasonSchema = z15.enum([
|
|
2120
|
+
"still-open",
|
|
2121
|
+
"reopened",
|
|
2122
|
+
"never-opened",
|
|
2123
|
+
"never-closed",
|
|
2124
|
+
"closed-before-records"
|
|
2125
|
+
]);
|
|
2126
|
+
var phaseRecordRecordSchema = z15.object({
|
|
2127
|
+
outcome: z15.literal("record"),
|
|
2128
|
+
phase: phaseNameSchema,
|
|
2129
|
+
record: buildRecordV2Schema
|
|
2130
|
+
}).strict().refine((body) => body.record.phase === body.phase, {
|
|
2131
|
+
message: "the record is for a different phase than the one asked for",
|
|
2132
|
+
path: ["record", "phase"]
|
|
2133
|
+
});
|
|
2134
|
+
var phaseRecordWithheldSchema = z15.object({
|
|
2135
|
+
outcome: z15.literal("withheld"),
|
|
2136
|
+
phase: phaseNameSchema,
|
|
2137
|
+
sequence: z15.number().int().min(1),
|
|
2138
|
+
closedAt: utcIso8601,
|
|
2139
|
+
message: z15.string()
|
|
2140
|
+
}).strict();
|
|
2141
|
+
var phaseRecordRefusedSchema = wireErrorSchema.extend({
|
|
2142
|
+
outcome: z15.literal("refused"),
|
|
2143
|
+
phase: phaseNameSchema,
|
|
2144
|
+
statusCode: z15.literal(409),
|
|
2145
|
+
error: z15.literal("PhaseRecordRefused"),
|
|
2146
|
+
reason: phaseRecordRefusalReasonSchema
|
|
2147
|
+
}).strict();
|
|
2148
|
+
var phaseRecordResponseSchema = z15.discriminatedUnion("outcome", [
|
|
2149
|
+
phaseRecordRecordSchema,
|
|
2150
|
+
phaseRecordWithheldSchema,
|
|
2151
|
+
phaseRecordRefusedSchema
|
|
2152
|
+
]);
|
|
2153
|
+
|
|
1653
2154
|
// dist/engagement-credential.js
|
|
1654
2155
|
import { chmodSync as chmodSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
1655
2156
|
import { platform as platform2 } from "node:os";
|
|
@@ -2005,17 +2506,17 @@ function fileSha256(bytes) {
|
|
|
2005
2506
|
const text = typeof bytes === "string" ? bytes : Buffer2.from(bytes).toString("utf-8");
|
|
2006
2507
|
return sha256Hex(text.split("\n").map(lineKey).join("\n"));
|
|
2007
2508
|
}
|
|
2008
|
-
function
|
|
2509
|
+
function canonicalJson2(value) {
|
|
2009
2510
|
if (Array.isArray(value))
|
|
2010
|
-
return value.map(
|
|
2511
|
+
return value.map(canonicalJson2);
|
|
2011
2512
|
if (value !== null && typeof value === "object") {
|
|
2012
|
-
const entries = Object.entries(value).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([k, v]) => [k,
|
|
2513
|
+
const entries = Object.entries(value).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([k, v]) => [k, canonicalJson2(v)]);
|
|
2013
2514
|
return Object.fromEntries(entries);
|
|
2014
2515
|
}
|
|
2015
2516
|
return value;
|
|
2016
2517
|
}
|
|
2017
2518
|
function canonicalSha256(value) {
|
|
2018
|
-
return sha256Hex(JSON.stringify(
|
|
2519
|
+
return sha256Hex(JSON.stringify(canonicalJson2(value)));
|
|
2019
2520
|
}
|
|
2020
2521
|
function byString(a, b) {
|
|
2021
2522
|
return a < b ? -1 : a > b ? 1 : 0;
|
|
@@ -2034,7 +2535,7 @@ function serializeManifest(manifest) {
|
|
|
2034
2535
|
leftAlone: [...manifest.leftAlone].sort(byString),
|
|
2035
2536
|
settings
|
|
2036
2537
|
};
|
|
2037
|
-
return JSON.stringify(
|
|
2538
|
+
return JSON.stringify(canonicalJson2(ordered), null, 2) + "\n";
|
|
2038
2539
|
}
|
|
2039
2540
|
var UnreadableManifestError = class extends Error {
|
|
2040
2541
|
constructor(reason) {
|
|
@@ -2540,7 +3041,7 @@ function recordSettingsMerge(draft, existing, generated, preexisted) {
|
|
|
2540
3041
|
const generatedHooks = generated.hooks ?? {};
|
|
2541
3042
|
const hookCommands = Object.values(generatedHooks).flatMap((entries) => entries.flatMap((entry) => entry.hooks.map((h) => h.command)));
|
|
2542
3043
|
const ours = /* @__PURE__ */ new Set([...hookCommands, ...HALFCYCLE_HOOK_COMMANDS]);
|
|
2543
|
-
const existingHooks =
|
|
3044
|
+
const existingHooks = isPlainObject2(existing.hooks) ? existing.hooks : void 0;
|
|
2544
3045
|
const heldOurs = Object.values(existingHooks ?? {}).some((entries) => Array.isArray(entries) && entries.some((entry) => isHalfcycleEntry(entry, ours)));
|
|
2545
3046
|
const existingDeny = Array.isArray(existing.permissions?.deny) ? existing.permissions.deny : [];
|
|
2546
3047
|
const schemaChanged = generated.$schema !== void 0 && existing.$schema !== generated.$schema;
|
|
@@ -2558,12 +3059,12 @@ function recordSettingsMerge(draft, existing, generated, preexisted) {
|
|
|
2558
3059
|
}
|
|
2559
3060
|
function recordMcpMerge(draft, existingText, writtenText) {
|
|
2560
3061
|
const before = existingText === null ? void 0 : JSON.parse(existingText);
|
|
2561
|
-
const servers =
|
|
3062
|
+
const servers = isPlainObject2(before) ? before["mcpServers"] : void 0;
|
|
2562
3063
|
const written = JSON.parse(writtenText);
|
|
2563
3064
|
recordMcp(draft, {
|
|
2564
3065
|
created: existingText === null,
|
|
2565
|
-
adopted: draft.previous === null &&
|
|
2566
|
-
mcpServersCreated: !(
|
|
3066
|
+
adopted: draft.previous === null && isPlainObject2(servers) && MCP_SERVER_KEY in servers,
|
|
3067
|
+
mcpServersCreated: !(isPlainObject2(before) && "mcpServers" in before),
|
|
2567
3068
|
entrySha256: canonicalSha256(written.mcpServers[MCP_SERVER_KEY])
|
|
2568
3069
|
});
|
|
2569
3070
|
}
|
|
@@ -2576,8 +3077,8 @@ function recordMcpAdoptionOnly(draft, existingText) {
|
|
|
2576
3077
|
} catch {
|
|
2577
3078
|
return;
|
|
2578
3079
|
}
|
|
2579
|
-
const servers =
|
|
2580
|
-
if (!
|
|
3080
|
+
const servers = isPlainObject2(before) ? before["mcpServers"] : void 0;
|
|
3081
|
+
if (!isPlainObject2(servers) || !(MCP_SERVER_KEY in servers))
|
|
2581
3082
|
return;
|
|
2582
3083
|
recordMcp(draft, { created: false, adopted: true, mcpServersCreated: false });
|
|
2583
3084
|
}
|
|
@@ -2662,7 +3163,7 @@ function readFileOrNull(path) {
|
|
|
2662
3163
|
return null;
|
|
2663
3164
|
}
|
|
2664
3165
|
}
|
|
2665
|
-
function
|
|
3166
|
+
function isPlainObject2(value) {
|
|
2666
3167
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
2667
3168
|
}
|
|
2668
3169
|
function readPluginManifest() {
|
|
@@ -3010,6 +3511,54 @@ printf '%s\\n' '${USER_PROMPT_REMINDER_SENTENCE}'
|
|
|
3010
3511
|
exit 0
|
|
3011
3512
|
`;
|
|
3012
3513
|
}
|
|
3514
|
+
var GENERATED_USER_PROMPT_SCOPE_HEADER = "# Typed scope approval \u2014 generated by";
|
|
3515
|
+
function generateUserPromptScopeHook() {
|
|
3516
|
+
return `#!/usr/bin/env bash
|
|
3517
|
+
${GENERATED_USER_PROMPT_SCOPE_HEADER} the Halfcycle installer.
|
|
3518
|
+
#
|
|
3519
|
+
# Lets the owner of this project approve a phase's scope, or send it back, by
|
|
3520
|
+
# typing "approve <phase>" or "send back <phase>: <note>" as the whole prompt in
|
|
3521
|
+
# their own Claude Code session.
|
|
3522
|
+
#
|
|
3523
|
+
# AN ORDINARY PROMPT STARTS NOTHING. This script reads the hook input once and
|
|
3524
|
+
# looks only at the start of the prompt's value. Unless the prompt begins, after
|
|
3525
|
+
# any whitespace (typed, or escaped in the input), with "approve " or
|
|
3526
|
+
# "send back " in any case, it exits 0 with no output, and Node never starts.
|
|
3527
|
+
# A quote inside any other field arrives escaped, so no other field (the folder
|
|
3528
|
+
# the session is in, for one) can read as the start of the prompt.
|
|
3529
|
+
#
|
|
3530
|
+
# Otherwise it finds the project root from its own location, never from the
|
|
3531
|
+
# folder the session has moved to, and hands the same input to the guard
|
|
3532
|
+
# wrapper beside it. That decides whether the line counts and prints at most
|
|
3533
|
+
# one plain line. Nothing here ever blocks a prompt.
|
|
3534
|
+
#
|
|
3535
|
+
# FAILS OPEN up to that hand-over: any error before it exits 0 with no output.
|
|
3536
|
+
# After it, the wrapper's own exit stands.
|
|
3537
|
+
trap 'exit 0' ERR
|
|
3538
|
+
set -e
|
|
3539
|
+
|
|
3540
|
+
# Read with the shell's own builtin, so the check below is the only program an
|
|
3541
|
+
# ordinary prompt starts.
|
|
3542
|
+
HC_INPUT=""
|
|
3543
|
+
IFS= read -r -d '' HC_INPUT || true
|
|
3544
|
+
|
|
3545
|
+
# What may come before the phrase: whitespace as typed, as a short escape, as a
|
|
3546
|
+
# \\u escape, or as the UTF-8 bytes of a Unicode space. Compared byte for byte.
|
|
3547
|
+
HC_LEAD='[[:space:]]|\\\\[nrtf]|\\\\u(000[9a-d]|0020|00a0|1680|200[0-9a]|202[89f]|205f|3000|feff)'
|
|
3548
|
+
HC_LEAD="$HC_LEAD|"$'\\xc2\\xa0|\\xe1\\x9a\\x80|\\xe2\\x80[\\x80-\\x8a\\xa8\\xa9\\xaf]|\\xe2\\x81\\x9f|\\xe3\\x80\\x80|\\xef\\xbb\\xbf'
|
|
3549
|
+
if ! printf '%s' "$HC_INPUT" | LC_ALL=C grep -Eiq "\\"prompt\\"[[:space:]]*:[[:space:]]*\\"($HC_LEAD)*(approve|send back) "; then
|
|
3550
|
+
exit 0
|
|
3551
|
+
fi
|
|
3552
|
+
|
|
3553
|
+
HC_SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
|
3554
|
+
HC_PROJECT_ROOT=$(CDPATH= cd -- "$HC_SCRIPT_DIR/../.." && pwd)
|
|
3555
|
+
export CLAUDE_PROJECT_DIR="$HC_PROJECT_ROOT"
|
|
3556
|
+
|
|
3557
|
+
trap - ERR
|
|
3558
|
+
set +e
|
|
3559
|
+
exec bash "$HC_PROJECT_ROOT/.claude/hooks/guard-runner.sh" hook user-prompt-submit <<<"$HC_INPUT"
|
|
3560
|
+
`;
|
|
3561
|
+
}
|
|
3013
3562
|
var CI_STANZA_KNOWN_HEADER_HASHES = [
|
|
3014
3563
|
"b225800b2d699811f32ac213238cbf81c35b5e6a74114bdf55e39cd9d7e2928f",
|
|
3015
3564
|
"674181735460f051af7c157222cf851fc3000fb3b8af3a10ea0bde9fe5eaba1f",
|
|
@@ -3085,6 +3634,7 @@ function removeRetiredMcpHeadersHelper(targetRepo) {
|
|
|
3085
3634
|
var OWNED_GENERATED_HEADERS = {
|
|
3086
3635
|
".claude/hooks/guard-runner.sh": GENERATED_GUARD_RUNNER_HEADER,
|
|
3087
3636
|
".claude/hooks/user-prompt-reminder.sh": GENERATED_USER_PROMPT_REMINDER_HEADER,
|
|
3637
|
+
".claude/hooks/user-prompt-scope.sh": GENERATED_USER_PROMPT_SCOPE_HEADER,
|
|
3088
3638
|
".claude/hooks/session-end-marker.sh": GENERATED_SESSION_END_HEADER,
|
|
3089
3639
|
".claude/hooks/session-start-marker.sh": GENERATED_SESSION_START_HEADER
|
|
3090
3640
|
};
|
|
@@ -3093,6 +3643,7 @@ function hookScriptsThisVersionWrites() {
|
|
|
3093
3643
|
[".claude/hooks/guard-runner.sh", generateGuardRunnerWrapper()],
|
|
3094
3644
|
[".claude/hooks/session-end-marker.sh", generateSessionEndMarker()],
|
|
3095
3645
|
[".claude/hooks/user-prompt-reminder.sh", generateUserPromptReminderHook()],
|
|
3646
|
+
[".claude/hooks/user-prompt-scope.sh", generateUserPromptScopeHook()],
|
|
3096
3647
|
[".claude/hooks/session-start-marker.sh", generateSessionStartMarker()]
|
|
3097
3648
|
];
|
|
3098
3649
|
}
|
|
@@ -3237,6 +3788,25 @@ function writeBundlePin(targetRepoRoot, version, engagementId, engagementType, c
|
|
|
3237
3788
|
const pinText = inLineEndingOf(readFileOrNull(pinPath), JSON.stringify(pin, null, 2) + "\n");
|
|
3238
3789
|
writeAllowlisted(pinPath, targetRepoRoot, pinText, writtenPaths);
|
|
3239
3790
|
}
|
|
3791
|
+
function updatePinnedMethodVersion(repoRoot, engagementId, version) {
|
|
3792
|
+
let pin;
|
|
3793
|
+
try {
|
|
3794
|
+
pin = readBundlePin(repoRoot);
|
|
3795
|
+
} catch {
|
|
3796
|
+
return;
|
|
3797
|
+
}
|
|
3798
|
+
if (pin === null || pin.engagementId !== engagementId)
|
|
3799
|
+
return;
|
|
3800
|
+
const pinPath = join5(repoRoot, BUNDLE_PIN_REL);
|
|
3801
|
+
const updated = { ...pin, methodVersion: version };
|
|
3802
|
+
const pinText = inLineEndingOf(readFileOrNull(pinPath), JSON.stringify(updated, null, 2) + "\n");
|
|
3803
|
+
try {
|
|
3804
|
+
writeAllowlisted(pinPath, repoRoot, pinText, []);
|
|
3805
|
+
} catch {
|
|
3806
|
+
process.stderr.write(`[halfcycle] Could not update ${BUNDLE_PIN_REL} with the new method version.
|
|
3807
|
+
`);
|
|
3808
|
+
}
|
|
3809
|
+
}
|
|
3240
3810
|
function writeCrewRoster(targetRepoRoot, report) {
|
|
3241
3811
|
const doc = {
|
|
3242
3812
|
$comment: "GENERATED by the Halfcycle installer \u2014 do not edit by hand. Re-run the installer to pick up a roster change.",
|
|
@@ -4851,10 +5421,10 @@ var SYSTEM_FOLDERS = [
|
|
|
4851
5421
|
"C:\\Program Files",
|
|
4852
5422
|
"C:\\Program Files (x86)"
|
|
4853
5423
|
];
|
|
4854
|
-
function refuseSystemFolder(targetRepo,
|
|
5424
|
+
function refuseSystemFolder(targetRepo, platform5 = process.platform) {
|
|
4855
5425
|
const key = (folder) => {
|
|
4856
5426
|
const real = realOrLiteral(folder);
|
|
4857
|
-
return
|
|
5427
|
+
return platform5 === "win32" ? real.toLowerCase() : real;
|
|
4858
5428
|
};
|
|
4859
5429
|
const target = key(targetRepo);
|
|
4860
5430
|
if (SYSTEM_FOLDERS.some((folder) => key(folder) === target)) {
|
|
@@ -4987,11 +5557,11 @@ import { execFileSync as execFileSync3 } from "node:child_process";
|
|
|
4987
5557
|
import { existsSync as existsSync5, realpathSync as realpathSync2 } from "node:fs";
|
|
4988
5558
|
import { delimiter as delimiter2, join as join7 } from "node:path";
|
|
4989
5559
|
function probeClaudeCodeVersion(timeoutMs, deps = {}) {
|
|
4990
|
-
const
|
|
5560
|
+
const platform5 = deps.platform ?? process.platform;
|
|
4991
5561
|
const exec = deps.exec ?? execFileSync3;
|
|
4992
5562
|
let raw;
|
|
4993
5563
|
try {
|
|
4994
|
-
raw =
|
|
5564
|
+
raw = platform5 === "win32" ? exec("cmd.exe", ["/d", "/s", "/c", '"claude --version"'], {
|
|
4995
5565
|
encoding: "utf-8",
|
|
4996
5566
|
timeout: timeoutMs,
|
|
4997
5567
|
stdio: ["ignore", "pipe", "ignore"],
|
|
@@ -5021,7 +5591,7 @@ var INSTALL_METHOD_RULES = [
|
|
|
5021
5591
|
];
|
|
5022
5592
|
function probeClaudeCodeInstallMethod(deps = {}) {
|
|
5023
5593
|
const env = deps.env ?? process.env;
|
|
5024
|
-
const
|
|
5594
|
+
const platform5 = deps.platform ?? process.platform;
|
|
5025
5595
|
const exists = deps.exists ?? existsSync5;
|
|
5026
5596
|
const realpath = deps.realpath ?? ((file) => {
|
|
5027
5597
|
try {
|
|
@@ -5030,7 +5600,7 @@ function probeClaudeCodeInstallMethod(deps = {}) {
|
|
|
5030
5600
|
return file;
|
|
5031
5601
|
}
|
|
5032
5602
|
});
|
|
5033
|
-
const names =
|
|
5603
|
+
const names = platform5 === "win32" ? ["claude.exe", "claude.cmd"] : ["claude"];
|
|
5034
5604
|
const dirs = (env["PATH"] ?? "").split(delimiter2).filter((dir) => dir !== "");
|
|
5035
5605
|
for (const dir of dirs) {
|
|
5036
5606
|
for (const name of names) {
|
|
@@ -5413,7 +5983,7 @@ function renderBuildRecordV2Markdown(record2) {
|
|
|
5413
5983
|
`Phase: ${phase} (the name the client gave it)`,
|
|
5414
5984
|
OBSERVED_LABEL,
|
|
5415
5985
|
[
|
|
5416
|
-
`- Method version: ${p.text(record2.observed.methodVersion)}. The Halfcycle method is updated from time to time, and each update has its own version number. This is the one the
|
|
5986
|
+
`- Method version: ${p.text(record2.observed.methodVersion)}. The Halfcycle method is updated from time to time, and each update has its own version number. This is the one the phase was run under. A project moves to a newer version only between phases.`,
|
|
5417
5987
|
`- Project identifier: ${p.text(record2.observed.engagementId)}. Halfcycle keeps this project's records under this identifier.`,
|
|
5418
5988
|
`- Record format: ${p.text(record2.format)}. This names the layout this file and the .json file beside it follow, for software that reads them.`
|
|
5419
5989
|
].join("\n"),
|
|
@@ -5672,6 +6242,10 @@ function refetchCommand(phase) {
|
|
|
5672
6242
|
// dist/open-phase.js
|
|
5673
6243
|
import { readFileSync as readFileSync8 } from "node:fs";
|
|
5674
6244
|
import { join as join9 } from "node:path";
|
|
6245
|
+
function readMethodUpdate(value) {
|
|
6246
|
+
const parsed = methodUpdateSchema.safeParse(value);
|
|
6247
|
+
return parsed.success ? parsed.data : null;
|
|
6248
|
+
}
|
|
5675
6249
|
function describeOpen(opened) {
|
|
5676
6250
|
const { phase, previousPhase } = opened;
|
|
5677
6251
|
if (previousPhase === phase) {
|
|
@@ -5781,13 +6355,18 @@ async function openPhase(credential, phase, entry, home) {
|
|
|
5781
6355
|
phase: body.phase,
|
|
5782
6356
|
previousPhase: body.previousPhase,
|
|
5783
6357
|
stampFailure,
|
|
5784
|
-
decision: body.decision
|
|
6358
|
+
decision: body.decision,
|
|
6359
|
+
methodUpdated: readMethodUpdate(body.methodUpdated)
|
|
5785
6360
|
};
|
|
5786
6361
|
}
|
|
5787
6362
|
|
|
5788
6363
|
// dist/close-phase.js
|
|
5789
6364
|
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
5790
6365
|
var NO_STATEMENTS = { thesis: null, outcome: null, deviations: [] };
|
|
6366
|
+
function readMethodUpdate2(value) {
|
|
6367
|
+
const parsed = methodUpdateSchema.safeParse(value);
|
|
6368
|
+
return parsed.success ? parsed.data : null;
|
|
6369
|
+
}
|
|
5791
6370
|
function readCloseCommit(repoRoot) {
|
|
5792
6371
|
if (repoRoot === void 0) {
|
|
5793
6372
|
return {
|
|
@@ -5876,7 +6455,13 @@ async function closePhase(credential, phase, outcome, close, home, repoRoot, sta
|
|
|
5876
6455
|
throw new Error(`[halfcycle] The response from ${url} did not carry { engagementId, status }. The phase may or may not have closed \u2014 read the engagement's status before retrying.`);
|
|
5877
6456
|
}
|
|
5878
6457
|
const stampFailure = writePhaseStamp(credential.engagementId, null, home);
|
|
5879
|
-
return {
|
|
6458
|
+
return {
|
|
6459
|
+
engagementId: answer.engagementId,
|
|
6460
|
+
status: answer.status,
|
|
6461
|
+
stampFailure,
|
|
6462
|
+
closeCommit,
|
|
6463
|
+
methodUpdated: readMethodUpdate2(answer.methodUpdated)
|
|
6464
|
+
};
|
|
5880
6465
|
}
|
|
5881
6466
|
function describeIssuePath(path) {
|
|
5882
6467
|
const [head, field] = path;
|
|
@@ -5965,8 +6550,8 @@ function unownedPatterns(description, tracked) {
|
|
|
5965
6550
|
}
|
|
5966
6551
|
return found;
|
|
5967
6552
|
}
|
|
5968
|
-
function unownedPatternLines(unowned) {
|
|
5969
|
-
return unowned.map((u) => `halfcycle
|
|
6553
|
+
function unownedPatternLines(unowned, verb) {
|
|
6554
|
+
return unowned.map((u) => `halfcycle ${verb}: warning: "${u.pattern}" (part "${u.partId}") matches no file in this repository. Correct the path, or remove it if the code moved.
|
|
5970
6555
|
`).join("");
|
|
5971
6556
|
}
|
|
5972
6557
|
function systemReportUrl(credential) {
|
|
@@ -6062,8 +6647,8 @@ async function checkAndReportDescription(repoRoot, deps, verb, beforeSend = () =
|
|
|
6062
6647
|
};
|
|
6063
6648
|
}
|
|
6064
6649
|
const unowned = unownedPatterns(checked.description, deps.listTracked(repoRoot));
|
|
6065
|
-
const warnings = unowned === null ? "" : unownedPatternLines(unowned);
|
|
6066
|
-
const refusal = beforeSend();
|
|
6650
|
+
const warnings = unowned === null ? "" : unownedPatternLines(unowned, verb);
|
|
6651
|
+
const refusal = beforeSend(checked.description);
|
|
6067
6652
|
if (refusal !== null)
|
|
6068
6653
|
return { kind: "stopped", stderr: warnings + refusal };
|
|
6069
6654
|
let credential;
|
|
@@ -6116,17 +6701,351 @@ async function runSystem(repoRoot, deps) {
|
|
|
6116
6701
|
}
|
|
6117
6702
|
}
|
|
6118
6703
|
|
|
6704
|
+
// dist/plan.js
|
|
6705
|
+
import { chmodSync as chmodSync3, mkdirSync as mkdirSync7, readFileSync as readFileSync10, renameSync as renameSync2, unlinkSync, writeFileSync as writeFileSync6 } from "node:fs";
|
|
6706
|
+
import { platform as platform4 } from "node:os";
|
|
6707
|
+
import { join as join11 } from "node:path";
|
|
6708
|
+
var PLAN_USAGE = `${HALFCYCLE_COMMAND} plan <phase> [--show] [--repo <root>]`;
|
|
6709
|
+
var PLAN_DIR_REL = "docs/phases";
|
|
6710
|
+
function planFileRel(phase) {
|
|
6711
|
+
return `${PLAN_DIR_REL}/${renderPhaseSegment(phase)}.plan.json`;
|
|
6712
|
+
}
|
|
6713
|
+
var PLAN_CHANGE_LABELS = {
|
|
6714
|
+
add: "Will be added",
|
|
6715
|
+
change: "Will change",
|
|
6716
|
+
remove: "Will be removed"
|
|
6717
|
+
};
|
|
6718
|
+
function parsePlanArgs(argv) {
|
|
6719
|
+
const positionals = [];
|
|
6720
|
+
let repoRoot;
|
|
6721
|
+
let show = false;
|
|
6722
|
+
for (let i = 0; i < argv.length; i++) {
|
|
6723
|
+
const arg = argv[i];
|
|
6724
|
+
if (arg === "--repo") {
|
|
6725
|
+
const value = argv[i + 1];
|
|
6726
|
+
if (repoRoot !== void 0 || value === void 0 || value === "" || value.startsWith("-"))
|
|
6727
|
+
return { ok: false };
|
|
6728
|
+
repoRoot = value;
|
|
6729
|
+
i++;
|
|
6730
|
+
continue;
|
|
6731
|
+
}
|
|
6732
|
+
if (arg === "--show") {
|
|
6733
|
+
if (show)
|
|
6734
|
+
return { ok: false };
|
|
6735
|
+
show = true;
|
|
6736
|
+
continue;
|
|
6737
|
+
}
|
|
6738
|
+
if (arg.startsWith("-"))
|
|
6739
|
+
return { ok: false };
|
|
6740
|
+
positionals.push(arg);
|
|
6741
|
+
}
|
|
6742
|
+
if (positionals.length !== 1)
|
|
6743
|
+
return { ok: false };
|
|
6744
|
+
return { ok: true, phase: positionals[0], show, repoRoot };
|
|
6745
|
+
}
|
|
6746
|
+
function planProblemPlace(raw, fileRel, path) {
|
|
6747
|
+
if (path === "")
|
|
6748
|
+
return fileRel;
|
|
6749
|
+
const [list, index] = path.split(".");
|
|
6750
|
+
const position = Number(index);
|
|
6751
|
+
if (list !== "changes" || !Number.isInteger(position) || position < 0)
|
|
6752
|
+
return path;
|
|
6753
|
+
const changes = typeof raw === "object" && raw !== null ? raw["changes"] : void 0;
|
|
6754
|
+
const item = Array.isArray(changes) ? changes[position] : void 0;
|
|
6755
|
+
const part = typeof item === "object" && item !== null ? item["part"] : void 0;
|
|
6756
|
+
return typeof part === "string" && part !== "" ? `${path} (part "${part}")` : `${path} (part ${position + 1})`;
|
|
6757
|
+
}
|
|
6758
|
+
function planProblemLines(raw, fileRel, problems) {
|
|
6759
|
+
return problems.map((p) => ` ${planProblemPlace(raw, fileRel, p.path)}: ${p.message}
|
|
6760
|
+
`).join("");
|
|
6761
|
+
}
|
|
6762
|
+
function readAndCheckPlan(repoRoot, phase, description, verb) {
|
|
6763
|
+
const p = `halfcycle ${verb}:`;
|
|
6764
|
+
const fileRel = planFileRel(phase);
|
|
6765
|
+
let text;
|
|
6766
|
+
try {
|
|
6767
|
+
text = readFileSync10(join11(repoRoot, fileRel), "utf-8");
|
|
6768
|
+
} catch (err) {
|
|
6769
|
+
const code = err.code;
|
|
6770
|
+
if (code === "ENOENT" || code === "ENOTDIR") {
|
|
6771
|
+
return {
|
|
6772
|
+
ok: false,
|
|
6773
|
+
line: `${p} there is no ${fileRel} in this project yet. Write the plan in the scoping step, then run this again. Nothing was sent.
|
|
6774
|
+
`
|
|
6775
|
+
};
|
|
6776
|
+
}
|
|
6777
|
+
return {
|
|
6778
|
+
ok: false,
|
|
6779
|
+
line: `${p} ${fileRel} could not be read. Check that it is a file you can open, then run this again. Nothing was sent.
|
|
6780
|
+
`
|
|
6781
|
+
};
|
|
6782
|
+
}
|
|
6783
|
+
let raw;
|
|
6784
|
+
try {
|
|
6785
|
+
raw = JSON.parse(text);
|
|
6786
|
+
} catch (err) {
|
|
6787
|
+
const why = err instanceof Error ? err.message : String(err);
|
|
6788
|
+
return { ok: false, line: `${p} ${fileRel} is not valid JSON (${why}). Fix it, then run this again. Nothing was sent.
|
|
6789
|
+
` };
|
|
6790
|
+
}
|
|
6791
|
+
const named = typeof raw === "object" && raw !== null ? raw["phase"] : void 0;
|
|
6792
|
+
if (isValidPhaseIdentity(named) && renderPhaseSegment(named) !== renderPhaseSegment(phase)) {
|
|
6793
|
+
return {
|
|
6794
|
+
ok: false,
|
|
6795
|
+
line: `${p} ${fileRel} is the plan for phase "${String(named)}", not phase "${phase}". Run this for the phase the plan names, or correct the plan's phase. Nothing was sent.
|
|
6796
|
+
`
|
|
6797
|
+
};
|
|
6798
|
+
}
|
|
6799
|
+
const checked = checkPhasePlan(raw, description);
|
|
6800
|
+
if (!checked.ok) {
|
|
6801
|
+
const n = checked.problems.length;
|
|
6802
|
+
return {
|
|
6803
|
+
ok: false,
|
|
6804
|
+
line: `${p} ${fileRel} has ${n} ${n === 1 ? "problem" : "problems"}, so nothing was sent:
|
|
6805
|
+
` + planProblemLines(raw, fileRel, checked.problems)
|
|
6806
|
+
};
|
|
6807
|
+
}
|
|
6808
|
+
return { ok: true, plan: checked.plan };
|
|
6809
|
+
}
|
|
6810
|
+
function planReportUrl(credential, phase) {
|
|
6811
|
+
return `${credential.serviceUrl}/engagements/${encodeURIComponent(credential.engagementId)}/phases/${encodeURIComponent(String(phase))}/plan`;
|
|
6812
|
+
}
|
|
6813
|
+
function serviceHost2(serviceUrl) {
|
|
6814
|
+
try {
|
|
6815
|
+
return new URL(serviceUrl).host;
|
|
6816
|
+
} catch {
|
|
6817
|
+
return serviceUrl;
|
|
6818
|
+
}
|
|
6819
|
+
}
|
|
6820
|
+
async function reportPhasePlan(credential, plan, descriptionVersion) {
|
|
6821
|
+
const body = { descriptionVersion, plan };
|
|
6822
|
+
let res;
|
|
6823
|
+
let text;
|
|
6824
|
+
try {
|
|
6825
|
+
res = await fetch(planReportUrl(credential, plan.phase), {
|
|
6826
|
+
method: "POST",
|
|
6827
|
+
headers: { authorization: `Bearer ${credential.token}`, "content-type": "application/json" },
|
|
6828
|
+
body: JSON.stringify(body)
|
|
6829
|
+
});
|
|
6830
|
+
text = await res.text();
|
|
6831
|
+
} catch {
|
|
6832
|
+
return {
|
|
6833
|
+
kind: "unavailable",
|
|
6834
|
+
why: `could not reach Halfcycle at ${serviceHost2(credential.serviceUrl)}. Check this machine's connection, then run this again.`
|
|
6835
|
+
};
|
|
6836
|
+
}
|
|
6837
|
+
let raw = null;
|
|
6838
|
+
try {
|
|
6839
|
+
raw = JSON.parse(text);
|
|
6840
|
+
} catch {
|
|
6841
|
+
raw = null;
|
|
6842
|
+
}
|
|
6843
|
+
const parsed = phasePlanResultSchema.safeParse(raw);
|
|
6844
|
+
if (parsed.success) {
|
|
6845
|
+
const answer = parsed.data;
|
|
6846
|
+
if (answer.outcome === "recorded") {
|
|
6847
|
+
return {
|
|
6848
|
+
kind: "recorded",
|
|
6849
|
+
version: answer.version,
|
|
6850
|
+
phaseNumber: answer.phaseNumber,
|
|
6851
|
+
state: answer.state,
|
|
6852
|
+
changedParts: answer.changedParts,
|
|
6853
|
+
typedApprovalOffered: answer.typedApprovalOffered
|
|
6854
|
+
};
|
|
6855
|
+
}
|
|
6856
|
+
if (answer.outcome === "refused")
|
|
6857
|
+
return { kind: "refused", problems: answer.problems };
|
|
6858
|
+
return {
|
|
6859
|
+
kind: "unavailable",
|
|
6860
|
+
why: "Halfcycle could not record the plan just now. Nothing was recorded; run this again in a minute."
|
|
6861
|
+
};
|
|
6862
|
+
}
|
|
6863
|
+
if (isMissingRoute("POST", res.status, raw))
|
|
6864
|
+
return { kind: "no-route" };
|
|
6865
|
+
if (res.ok) {
|
|
6866
|
+
return {
|
|
6867
|
+
kind: "unavailable",
|
|
6868
|
+
why: "Halfcycle sent an answer this command could not read. The plan may not be recorded; run this again later."
|
|
6869
|
+
};
|
|
6870
|
+
}
|
|
6871
|
+
const message = typeof raw === "object" && raw !== null && typeof raw.message === "string" ? raw.message : null;
|
|
6872
|
+
return {
|
|
6873
|
+
kind: "unavailable",
|
|
6874
|
+
why: `Halfcycle answered ${res.status}${message === null ? "." : `: ${message}`}`
|
|
6875
|
+
};
|
|
6876
|
+
}
|
|
6877
|
+
function writeShownPlan(engagementId, entry, home) {
|
|
6878
|
+
let temp = null;
|
|
6879
|
+
try {
|
|
6880
|
+
const dir = engagementStateDir(engagementId, home);
|
|
6881
|
+
const path = join11(dir, SHOWN_PLANS_FILENAME);
|
|
6882
|
+
let existing = [];
|
|
6883
|
+
try {
|
|
6884
|
+
existing = parseShownPlans(readFileSync10(path, "utf-8"));
|
|
6885
|
+
} catch {
|
|
6886
|
+
existing = [];
|
|
6887
|
+
}
|
|
6888
|
+
const kept = existing.filter((e) => e.phase !== entry.phase && e.number !== entry.number);
|
|
6889
|
+
const entries = [...kept, entry];
|
|
6890
|
+
const body = serialiseShownPlans(entries);
|
|
6891
|
+
mkdirSync7(dir, { recursive: true, mode: 448 });
|
|
6892
|
+
temp = join11(dir, `.${SHOWN_PLANS_FILENAME}.${process.pid}.tmp`);
|
|
6893
|
+
writeFileSync6(temp, body, { mode: 384 });
|
|
6894
|
+
if (platform4() !== "win32")
|
|
6895
|
+
chmodSync3(temp, 384);
|
|
6896
|
+
renameSync2(temp, path);
|
|
6897
|
+
return { ok: true, entries };
|
|
6898
|
+
} catch {
|
|
6899
|
+
if (temp !== null) {
|
|
6900
|
+
try {
|
|
6901
|
+
unlinkSync(temp);
|
|
6902
|
+
} catch {
|
|
6903
|
+
}
|
|
6904
|
+
}
|
|
6905
|
+
return { ok: false };
|
|
6906
|
+
}
|
|
6907
|
+
}
|
|
6908
|
+
function readPinnedClientVersion(repoRoot) {
|
|
6909
|
+
try {
|
|
6910
|
+
const version = readBundlePin(repoRoot)?.version;
|
|
6911
|
+
return typeof version === "string" ? version : void 0;
|
|
6912
|
+
} catch {
|
|
6913
|
+
return void 0;
|
|
6914
|
+
}
|
|
6915
|
+
}
|
|
6916
|
+
function planRefusedLines(verb, phase, plan, problems) {
|
|
6917
|
+
const fileRel = planFileRel(phase);
|
|
6918
|
+
const n = problems.length;
|
|
6919
|
+
return `halfcycle ${verb}: Halfcycle refused ${fileRel} (${n} ${n === 1 ? "problem" : "problems"}); the plan was not recorded:
|
|
6920
|
+
` + planProblemLines(plan, fileRel, problems);
|
|
6921
|
+
}
|
|
6922
|
+
function notAPhaseNameLine(verb, phase) {
|
|
6923
|
+
return `halfcycle ${verb}: "${phase}" is not a phase name: a name cannot be empty, start with a dot, or hold a slash, a backslash or a control character. Nothing was sent.
|
|
6924
|
+
`;
|
|
6925
|
+
}
|
|
6926
|
+
function planStateSentence(state) {
|
|
6927
|
+
switch (state) {
|
|
6928
|
+
case "waiting":
|
|
6929
|
+
return "Waiting for the owner's approval.";
|
|
6930
|
+
case "approved":
|
|
6931
|
+
return "The owner has approved this plan.";
|
|
6932
|
+
case "partly-stale":
|
|
6933
|
+
return "The owner approved an earlier plan; the parts marked as changed wait for the owner's approval.";
|
|
6934
|
+
case "sent-back":
|
|
6935
|
+
return "The owner sent this plan back. Change it to answer the owner's notes, then run this again.";
|
|
6936
|
+
case "withdrawn":
|
|
6937
|
+
return "The owner withdrew their approval of this plan. It waits for the owner's approval again.";
|
|
6938
|
+
}
|
|
6939
|
+
}
|
|
6940
|
+
function approvalLine(phase, number, typedApprovalOffered, pinnedVersion, shown2) {
|
|
6941
|
+
if (!typedApprovalOffered)
|
|
6942
|
+
return TYPED_APPROVAL_OFF_SENTENCE;
|
|
6943
|
+
if (!clientMeetsChatApprovalFloor(pinnedVersion))
|
|
6944
|
+
return CLIENT_UPDATE_SENTENCE;
|
|
6945
|
+
const byNumber = String(number);
|
|
6946
|
+
const numberReachesThis = byNumber !== phase && resolvePhaseReference(byNumber, shown2)?.phase === phase;
|
|
6947
|
+
const typed = numberReachesThis ? `\`approve ${phase}\` (or \`approve ${byNumber}\`)` : `\`approve ${phase}\``;
|
|
6948
|
+
return `To approve it, the project's owner types ${typed} in their own Claude Code session. Only the owner can.`;
|
|
6949
|
+
}
|
|
6950
|
+
function changeLines(plan, description, changed) {
|
|
6951
|
+
const names = new Map(description.parts.map((part) => [part.id, part.name]));
|
|
6952
|
+
const marked = new Set(changed);
|
|
6953
|
+
return plan.changes.map((c) => {
|
|
6954
|
+
const mark = marked.has(c.part) ? " (changed since the owner last decided)" : "";
|
|
6955
|
+
return ` - ${names.get(c.part) ?? c.part} \xB7 ${PLAN_CHANGE_LABELS[c.change]} \xB7 ${c.summary}${mark}
|
|
6956
|
+
`;
|
|
6957
|
+
}).join("");
|
|
6958
|
+
}
|
|
6959
|
+
async function runPlan(repoRoot, phase, show, deps) {
|
|
6960
|
+
const p = "halfcycle plan:";
|
|
6961
|
+
if (!isValidPhaseIdentity(phase))
|
|
6962
|
+
return { stdout: "", stderr: notAPhaseNameLine("plan", phase), exitCode: 1 };
|
|
6963
|
+
let checkedPlan = null;
|
|
6964
|
+
let checkedDescription = null;
|
|
6965
|
+
const stage = await checkAndReportDescription(repoRoot, deps, "plan", (description2) => {
|
|
6966
|
+
const checked = readAndCheckPlan(repoRoot, phase, description2, "plan");
|
|
6967
|
+
if (!checked.ok)
|
|
6968
|
+
return checked.line;
|
|
6969
|
+
checkedPlan = checked.plan;
|
|
6970
|
+
checkedDescription = description2;
|
|
6971
|
+
return null;
|
|
6972
|
+
});
|
|
6973
|
+
if (stage.kind === "stopped")
|
|
6974
|
+
return { stdout: "", stderr: stage.stderr, exitCode: 1 };
|
|
6975
|
+
const cannotTakeYet = `${p} this Halfcycle service cannot take a phase plan yet. Nothing was recorded.
|
|
6976
|
+
`;
|
|
6977
|
+
if (stage.kind === "no-route")
|
|
6978
|
+
return { stdout: "", stderr: stage.warnings + cannotTakeYet, exitCode: 1 };
|
|
6979
|
+
const warnings = stage.warnings;
|
|
6980
|
+
const plan = checkedPlan;
|
|
6981
|
+
const description = checkedDescription;
|
|
6982
|
+
if (plan === null || description === null) {
|
|
6983
|
+
return { stdout: "", stderr: warnings + `${p} the plan was not checked, so nothing more was sent.
|
|
6984
|
+
`, exitCode: 1 };
|
|
6985
|
+
}
|
|
6986
|
+
const outcome = await deps.reportPlan(stage.credential, plan, stage.version);
|
|
6987
|
+
switch (outcome.kind) {
|
|
6988
|
+
case "no-route":
|
|
6989
|
+
return { stdout: "", stderr: warnings + cannotTakeYet, exitCode: 1 };
|
|
6990
|
+
case "unavailable":
|
|
6991
|
+
return { stdout: "", stderr: warnings + `${p} ${outcome.why}
|
|
6992
|
+
`, exitCode: 1 };
|
|
6993
|
+
case "refused":
|
|
6994
|
+
return { stdout: "", stderr: warnings + planRefusedLines("plan", phase, plan, outcome.problems), exitCode: 1 };
|
|
6995
|
+
case "recorded":
|
|
6996
|
+
break;
|
|
6997
|
+
}
|
|
6998
|
+
const shownPhase = String(plan.phase);
|
|
6999
|
+
const recorded = `[halfcycle] Plan for ${shownPhase} recorded: plan version ${outcome.version} (phase ${outcome.phaseNumber}).`;
|
|
7000
|
+
const stateSentence = planStateSentence(outcome.state);
|
|
7001
|
+
if (!show)
|
|
7002
|
+
return { stdout: `${recorded} ${stateSentence}
|
|
7003
|
+
`, stderr: warnings, exitCode: 0 };
|
|
7004
|
+
const entry = {
|
|
7005
|
+
phase: shownPhase,
|
|
7006
|
+
number: outcome.phaseNumber,
|
|
7007
|
+
version: outcome.version,
|
|
7008
|
+
shownAt: deps.now().toISOString()
|
|
7009
|
+
};
|
|
7010
|
+
const written = deps.recordShown(stage.credential.engagementId, entry);
|
|
7011
|
+
const listed = `${recorded}
|
|
7012
|
+
` + changeLines(plan, description, outcome.changedParts) + `${stateSentence}
|
|
7013
|
+
`;
|
|
7014
|
+
if (!written.ok) {
|
|
7015
|
+
return {
|
|
7016
|
+
stdout: listed,
|
|
7017
|
+
stderr: warnings + `${p} the list above could not be recorded on this machine, so typing an approval for it would not work. Check that the .halfcycle folder in your home folder can be written to, then run this again.
|
|
7018
|
+
`,
|
|
7019
|
+
exitCode: 1
|
|
7020
|
+
};
|
|
7021
|
+
}
|
|
7022
|
+
const stdout = listed + `${approvalLine(shownPhase, outcome.phaseNumber, outcome.typedApprovalOffered, deps.pinnedClientVersion(repoRoot), written.entries)}
|
|
7023
|
+
`;
|
|
7024
|
+
return { stdout, stderr: warnings, exitCode: 0 };
|
|
7025
|
+
}
|
|
7026
|
+
|
|
6119
7027
|
// dist/draw/write.js
|
|
6120
|
-
import { closeSync as closeSync2, lstatSync as lstatSync2, mkdirSync as
|
|
6121
|
-
import { dirname as dirname5, isAbsolute as isAbsolute3, join as
|
|
7028
|
+
import { closeSync as closeSync2, lstatSync as lstatSync2, mkdirSync as mkdirSync8, openSync as openSync2, readFileSync as readFileSync11, readSync, realpathSync as realpathSync3, rmSync as rmSync4 } from "node:fs";
|
|
7029
|
+
import { dirname as dirname5, isAbsolute as isAbsolute3, join as join12, relative as relative4, resolve as resolve3, sep } from "node:path";
|
|
6122
7030
|
var DOCS_DIR = "docs";
|
|
6123
7031
|
var DIAGRAMS_DIR = "diagrams";
|
|
6124
7032
|
var DIAGRAM_DIR_REL = `${DOCS_DIR}/${DIAGRAMS_DIR}`;
|
|
6125
7033
|
var ARCHITECTURE_NAME = "architecture";
|
|
6126
|
-
var
|
|
6127
|
-
|
|
6128
|
-
|
|
6129
|
-
}
|
|
7034
|
+
var ARCHITECTURE_BASE = ARCHITECTURE_NAME;
|
|
7035
|
+
function phaseDiagramBase(phase) {
|
|
7036
|
+
return renderPhaseSegment(phase);
|
|
7037
|
+
}
|
|
7038
|
+
var FLIGHTDECK_SUFFIX = ".flightdeck";
|
|
7039
|
+
function baseTakesFlightDeckNames(base) {
|
|
7040
|
+
return base.toLowerCase().endsWith(FLIGHTDECK_SUFFIX);
|
|
7041
|
+
}
|
|
7042
|
+
function pictureNames(base) {
|
|
7043
|
+
const names = {
|
|
7044
|
+
broadsheet: base,
|
|
7045
|
+
flightdeck: `${base}${FLIGHTDECK_SUFFIX}`
|
|
7046
|
+
};
|
|
7047
|
+
return names;
|
|
7048
|
+
}
|
|
6130
7049
|
var EMBEDDED_LOOK = "broadsheet";
|
|
6131
7050
|
var DIAGRAM_MARKER_LINE = SYSTEM_DIAGRAM_MARKER_LINE;
|
|
6132
7051
|
var PNG_SIGNATURE = Uint8Array.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
@@ -6141,22 +7060,35 @@ var DIAGRAM_FILES = [
|
|
|
6141
7060
|
...SYSTEM_PICTURE_THEMES.map(pngOf),
|
|
6142
7061
|
"mmd"
|
|
6143
7062
|
];
|
|
6144
|
-
|
|
6145
|
-
|
|
6146
|
-
|
|
6147
|
-
|
|
6148
|
-
|
|
6149
|
-
|
|
6150
|
-
])
|
|
6151
|
-
|
|
6152
|
-
|
|
7063
|
+
function fileNames(base) {
|
|
7064
|
+
const looks = pictureNames(base);
|
|
7065
|
+
return Object.fromEntries([
|
|
7066
|
+
...SYSTEM_PICTURE_THEMES.flatMap((look) => [
|
|
7067
|
+
[svgOf(look), `${looks[look]}.svg`],
|
|
7068
|
+
[pngOf(look), `${looks[look]}.png`]
|
|
7069
|
+
]),
|
|
7070
|
+
["mmd", `${base}.mmd`]
|
|
7071
|
+
]);
|
|
7072
|
+
}
|
|
7073
|
+
function embedImageName(base) {
|
|
7074
|
+
return fileNames(base)[pngOf(EMBEDDED_LOOK)];
|
|
7075
|
+
}
|
|
7076
|
+
function markdownLinkName(name) {
|
|
7077
|
+
return encodeURIComponent(name).replace(/\(/g, "%28").replace(/\)/g, "%29");
|
|
7078
|
+
}
|
|
7079
|
+
function embedImageLink(base, doc) {
|
|
7080
|
+
const up = doc === "docs" ? "" : "../";
|
|
7081
|
+
return `${up}${DIAGRAMS_DIR}/${markdownLinkName(embedImageName(base))}`;
|
|
7082
|
+
}
|
|
7083
|
+
function diagramPaths(repoRoot, base) {
|
|
6153
7084
|
const root = resolve3(repoRoot);
|
|
6154
|
-
const dir =
|
|
6155
|
-
const
|
|
7085
|
+
const dir = join12(root, DOCS_DIR, DIAGRAMS_DIR);
|
|
7086
|
+
const names = fileNames(base);
|
|
7087
|
+
const file = Object.fromEntries(DIAGRAM_FILES.map((which) => [which, join12(dir, names[which])]));
|
|
6156
7088
|
return { repoRoot: root, dir, file };
|
|
6157
7089
|
}
|
|
6158
|
-
function diagramRel(which) {
|
|
6159
|
-
return `${DIAGRAM_DIR_REL}/${
|
|
7090
|
+
function diagramRel(base, which) {
|
|
7091
|
+
return `${DIAGRAM_DIR_REL}/${fileNames(base)[which]}`;
|
|
6160
7092
|
}
|
|
6161
7093
|
var DIAGRAM_FILES_AS_LISTED = [
|
|
6162
7094
|
...SYSTEM_PICTURE_THEMES.flatMap((look) => [pngOf(look), svgOf(look)]),
|
|
@@ -6247,7 +7179,7 @@ function readExistingSet(paths) {
|
|
|
6247
7179
|
if (!stat.isFile())
|
|
6248
7180
|
return { ok: false, inTheWay: which, temp: false };
|
|
6249
7181
|
try {
|
|
6250
|
-
found[which] =
|
|
7182
|
+
found[which] = readFileSync11(paths.file[which]);
|
|
6251
7183
|
} catch {
|
|
6252
7184
|
return { ok: false, inTheWay: which, temp: false };
|
|
6253
7185
|
}
|
|
@@ -6293,15 +7225,15 @@ function readExistingSet(paths) {
|
|
|
6293
7225
|
function tempPath(paths, which) {
|
|
6294
7226
|
return `${paths.file[which]}.tmp`;
|
|
6295
7227
|
}
|
|
6296
|
-
function inTheWayLine(which, temp = false) {
|
|
6297
|
-
return `${diagramRel(which)}${temp ? ".tmp" : ""} is already there and was not drawn by this command. Move it or delete it, then run this again. Nothing was written.`;
|
|
7228
|
+
function inTheWayLine(base, which, temp = false) {
|
|
7229
|
+
return `${diagramRel(base, which)}${temp ? ".tmp" : ""} is already there and was not drawn by this command. Move it or delete it, then run this again. Nothing was written.`;
|
|
6298
7230
|
}
|
|
6299
|
-
function checkPictures(drawn) {
|
|
7231
|
+
function checkPictures(drawn, base) {
|
|
6300
7232
|
const set = {};
|
|
6301
7233
|
for (const look of SYSTEM_PICTURE_THEMES) {
|
|
6302
7234
|
const picture = drawn.pictures[look];
|
|
6303
|
-
const pngFor = diagramRel(pngOf(look));
|
|
6304
|
-
const svgFor = diagramRel(svgOf(look));
|
|
7235
|
+
const pngFor = diagramRel(base, pngOf(look));
|
|
7236
|
+
const svgFor = diagramRel(base, svgOf(look));
|
|
6305
7237
|
const png = Buffer.from(picture.png, "base64");
|
|
6306
7238
|
if (png.length > SYSTEM_DRAW_PNG_MAX_BYTES)
|
|
6307
7239
|
return { ok: false, why: `the image for ${pngFor} is larger than 8 MB` };
|
|
@@ -6337,8 +7269,8 @@ function removeQuietly(path) {
|
|
|
6337
7269
|
function sameBytes(a, b) {
|
|
6338
7270
|
return a !== null && Buffer.from(a).equals(b);
|
|
6339
7271
|
}
|
|
6340
|
-
function writeDiagramSet(repoRoot, set, io = NODE_RECORD_PAIR_IO) {
|
|
6341
|
-
const paths = diagramPaths(repoRoot);
|
|
7272
|
+
function writeDiagramSet(repoRoot, base, set, io = NODE_RECORD_PAIR_IO) {
|
|
7273
|
+
const paths = diagramPaths(repoRoot, base);
|
|
6342
7274
|
const place = checkDiagramPlace(paths);
|
|
6343
7275
|
if (!place.ok)
|
|
6344
7276
|
return { kind: "unsafe", why: place.why };
|
|
@@ -6350,7 +7282,7 @@ function writeDiagramSet(repoRoot, set, io = NODE_RECORD_PAIR_IO) {
|
|
|
6350
7282
|
if (changed.length === 0)
|
|
6351
7283
|
return { kind: "up-to-date" };
|
|
6352
7284
|
try {
|
|
6353
|
-
|
|
7285
|
+
mkdirSync8(paths.dir, { recursive: true });
|
|
6354
7286
|
} catch {
|
|
6355
7287
|
return { kind: "failed", file: changed[0], removed: [] };
|
|
6356
7288
|
}
|
|
@@ -6411,7 +7343,7 @@ function restore(io, paths, renamed, existing) {
|
|
|
6411
7343
|
}
|
|
6412
7344
|
|
|
6413
7345
|
// dist/draw/index.js
|
|
6414
|
-
var DRAW_USAGE = `${HALFCYCLE_COMMAND} draw architecture [--repo <root>]`;
|
|
7346
|
+
var DRAW_USAGE = `${HALFCYCLE_COMMAND} draw (architecture | phase <phase>) [--repo <root>]`;
|
|
6415
7347
|
function parseDrawArgs(argv) {
|
|
6416
7348
|
const positionals = [];
|
|
6417
7349
|
let repoRoot;
|
|
@@ -6429,35 +7361,38 @@ function parseDrawArgs(argv) {
|
|
|
6429
7361
|
return { ok: false };
|
|
6430
7362
|
positionals.push(arg);
|
|
6431
7363
|
}
|
|
6432
|
-
if (positionals.length
|
|
6433
|
-
return { ok:
|
|
6434
|
-
|
|
7364
|
+
if (positionals.length === 1 && positionals[0] === "architecture") {
|
|
7365
|
+
return { ok: true, picture: { kind: "architecture" }, repoRoot };
|
|
7366
|
+
}
|
|
7367
|
+
if (positionals.length === 2 && positionals[0] === "phase") {
|
|
7368
|
+
return { ok: true, picture: { kind: "phase", phase: positionals[1] }, repoRoot };
|
|
7369
|
+
}
|
|
7370
|
+
return { ok: false };
|
|
6435
7371
|
}
|
|
6436
7372
|
function systemDrawUrl(credential) {
|
|
6437
7373
|
return `${credential.serviceUrl}/engagements/${encodeURIComponent(credential.engagementId)}/system/draw`;
|
|
6438
7374
|
}
|
|
6439
|
-
function
|
|
7375
|
+
function serviceHost3(serviceUrl) {
|
|
6440
7376
|
try {
|
|
6441
7377
|
return new URL(serviceUrl).host;
|
|
6442
7378
|
} catch {
|
|
6443
7379
|
return serviceUrl;
|
|
6444
7380
|
}
|
|
6445
7381
|
}
|
|
6446
|
-
async function requestDrawing(credential,
|
|
6447
|
-
const body = { version, picture: "architecture" };
|
|
7382
|
+
async function requestDrawing(credential, request) {
|
|
6448
7383
|
let res;
|
|
6449
7384
|
let text;
|
|
6450
7385
|
try {
|
|
6451
7386
|
res = await fetch(systemDrawUrl(credential), {
|
|
6452
7387
|
method: "POST",
|
|
6453
7388
|
headers: { authorization: `Bearer ${credential.token}`, "content-type": "application/json" },
|
|
6454
|
-
body: JSON.stringify(
|
|
7389
|
+
body: JSON.stringify(request)
|
|
6455
7390
|
});
|
|
6456
7391
|
text = await res.text();
|
|
6457
7392
|
} catch {
|
|
6458
7393
|
return {
|
|
6459
7394
|
kind: "unavailable",
|
|
6460
|
-
why: `could not reach Halfcycle at ${
|
|
7395
|
+
why: `could not reach Halfcycle at ${serviceHost3(credential.serviceUrl)}. Check this machine's connection, then run this again. Nothing was written.`
|
|
6461
7396
|
};
|
|
6462
7397
|
}
|
|
6463
7398
|
let raw = null;
|
|
@@ -6495,39 +7430,101 @@ async function requestDrawing(credential, version) {
|
|
|
6495
7430
|
var P = "halfcycle draw:";
|
|
6496
7431
|
var CANNOT_DRAW_YET = `${P} this Halfcycle service cannot draw pictures yet. Nothing was written.
|
|
6497
7432
|
`;
|
|
6498
|
-
|
|
7433
|
+
var CANNOT_DRAW_PHASE_YET = `${P} this Halfcycle service cannot draw phase pictures yet. Nothing was written.
|
|
7434
|
+
`;
|
|
7435
|
+
function markdownAltText(text) {
|
|
7436
|
+
return text.replace(/[[\]\\]/g, (ch) => `\\${ch}`);
|
|
7437
|
+
}
|
|
7438
|
+
var ARCHITECTURE_PASTE = { doc: "docs", alt: "Architecture", base: ARCHITECTURE_BASE };
|
|
7439
|
+
function phasePaste(phase) {
|
|
7440
|
+
return { doc: "docs/phases", alt: `Phase ${phase}`, base: phaseDiagramBase(phase) };
|
|
7441
|
+
}
|
|
7442
|
+
function pasteLines(mermaid, target) {
|
|
6499
7443
|
const block = mermaid.endsWith("\n") ? mermaid : `${mermaid}
|
|
6500
7444
|
`;
|
|
7445
|
+
const where = target.doc === "docs" ? "a document directly under docs/" : "this phase's document in docs/phases/";
|
|
6501
7446
|
return `
|
|
6502
|
-
Paste this into
|
|
7447
|
+
Paste this into ${where}:
|
|
6503
7448
|
|
|
6504
|
-
})
|
|
6505
7450
|
|
|
6506
7451
|
\`\`\`mermaid
|
|
6507
7452
|
` + block + "```\n";
|
|
6508
7453
|
}
|
|
6509
|
-
function localRefusal(repoRoot) {
|
|
6510
|
-
const paths = diagramPaths(repoRoot);
|
|
7454
|
+
function localRefusal(repoRoot, base) {
|
|
7455
|
+
const paths = diagramPaths(repoRoot, base);
|
|
6511
7456
|
const place = checkDiagramPlace(paths);
|
|
6512
7457
|
if (!place.ok)
|
|
6513
7458
|
return `${P} ${place.why}
|
|
6514
7459
|
`;
|
|
6515
7460
|
const read = readExistingSet(paths);
|
|
6516
7461
|
if (!read.ok)
|
|
6517
|
-
return `${P} ${inTheWayLine(read.inTheWay, read.temp)}
|
|
7462
|
+
return `${P} ${inTheWayLine(base, read.inTheWay, read.temp)}
|
|
6518
7463
|
`;
|
|
6519
7464
|
return null;
|
|
6520
7465
|
}
|
|
6521
|
-
async function runDraw(repoRoot, deps) {
|
|
6522
|
-
|
|
7466
|
+
async function runDraw(repoRoot, picture, deps) {
|
|
7467
|
+
if (picture.kind === "architecture") {
|
|
7468
|
+
const stage2 = await checkAndReportDescription(repoRoot, deps, "draw", () => localRefusal(repoRoot, ARCHITECTURE_BASE));
|
|
7469
|
+
if (stage2.kind === "stopped")
|
|
7470
|
+
return { stdout: "", stderr: stage2.stderr, exitCode: 1 };
|
|
7471
|
+
if (stage2.kind === "no-route")
|
|
7472
|
+
return { stdout: "", stderr: stage2.warnings + CANNOT_DRAW_YET, exitCode: 1 };
|
|
7473
|
+
return drawAndWrite(repoRoot, deps, stage2.credential, { version: stage2.version, picture: "architecture" }, ARCHITECTURE_PASTE, stage2.warnings, CANNOT_DRAW_YET);
|
|
7474
|
+
}
|
|
7475
|
+
const phase = picture.phase;
|
|
7476
|
+
if (!isValidPhaseIdentity(phase))
|
|
7477
|
+
return { stdout: "", stderr: notAPhaseNameLine("draw", phase), exitCode: 1 };
|
|
7478
|
+
const base = phaseDiagramBase(phase);
|
|
7479
|
+
if (baseTakesFlightDeckNames(base)) {
|
|
7480
|
+
return {
|
|
7481
|
+
stdout: "",
|
|
7482
|
+
stderr: `${P} the phase "${phase}" cannot be drawn: its pictures would take the names of another phase's Flight Deck pictures. Name the phase so it does not end in ".flightdeck", then run this again. Nothing was sent.
|
|
7483
|
+
`,
|
|
7484
|
+
exitCode: 1
|
|
7485
|
+
};
|
|
7486
|
+
}
|
|
7487
|
+
let checkedPlan = null;
|
|
7488
|
+
const beforeSend = (description) => {
|
|
7489
|
+
const checked = readAndCheckPlan(repoRoot, phase, description, "draw");
|
|
7490
|
+
if (!checked.ok)
|
|
7491
|
+
return checked.line;
|
|
7492
|
+
const refusal = localRefusal(repoRoot, base);
|
|
7493
|
+
if (refusal !== null)
|
|
7494
|
+
return refusal;
|
|
7495
|
+
checkedPlan = checked.plan;
|
|
7496
|
+
return null;
|
|
7497
|
+
};
|
|
7498
|
+
const stage = await checkAndReportDescription(repoRoot, deps, "draw", beforeSend);
|
|
6523
7499
|
if (stage.kind === "stopped")
|
|
6524
7500
|
return { stdout: "", stderr: stage.stderr, exitCode: 1 };
|
|
6525
7501
|
if (stage.kind === "no-route")
|
|
6526
|
-
return { stdout: "", stderr: stage.warnings +
|
|
7502
|
+
return { stdout: "", stderr: stage.warnings + CANNOT_DRAW_PHASE_YET, exitCode: 1 };
|
|
6527
7503
|
const warnings = stage.warnings;
|
|
6528
|
-
const
|
|
7504
|
+
const plan = checkedPlan;
|
|
7505
|
+
if (plan === null) {
|
|
7506
|
+
return { stdout: "", stderr: warnings + `${P} the plan was not checked, so nothing more was sent.
|
|
7507
|
+
`, exitCode: 1 };
|
|
7508
|
+
}
|
|
7509
|
+
const reported = await deps.reportPlan(stage.credential, plan, stage.version);
|
|
7510
|
+
switch (reported.kind) {
|
|
7511
|
+
case "no-route":
|
|
7512
|
+
return { stdout: "", stderr: warnings + CANNOT_DRAW_PHASE_YET, exitCode: 1 };
|
|
7513
|
+
case "unavailable":
|
|
7514
|
+
return { stdout: "", stderr: warnings + `${P} ${reported.why}
|
|
7515
|
+
`, exitCode: 1 };
|
|
7516
|
+
case "refused":
|
|
7517
|
+
return { stdout: "", stderr: warnings + planRefusedLines("draw", phase, plan, reported.problems), exitCode: 1 };
|
|
7518
|
+
case "recorded":
|
|
7519
|
+
break;
|
|
7520
|
+
}
|
|
7521
|
+
return drawAndWrite(repoRoot, deps, stage.credential, { picture: "phase", phase: plan.phase, planVersion: reported.version }, phasePaste(phase), warnings, CANNOT_DRAW_PHASE_YET);
|
|
7522
|
+
}
|
|
7523
|
+
async function drawAndWrite(repoRoot, deps, credential, request, target, warnings, cannotDrawYet) {
|
|
7524
|
+
const base = target.base;
|
|
7525
|
+
const outcome = await deps.draw(credential, request);
|
|
6529
7526
|
if (outcome.kind === "no-route")
|
|
6530
|
-
return { stdout: "", stderr: warnings +
|
|
7527
|
+
return { stdout: "", stderr: warnings + cannotDrawYet, exitCode: 1 };
|
|
6531
7528
|
if (outcome.kind === "refused") {
|
|
6532
7529
|
return {
|
|
6533
7530
|
stdout: "",
|
|
@@ -6539,7 +7536,7 @@ async function runDraw(repoRoot, deps) {
|
|
|
6539
7536
|
if (outcome.kind === "unavailable")
|
|
6540
7537
|
return { stdout: "", stderr: warnings + `${P} ${outcome.why}
|
|
6541
7538
|
`, exitCode: 1 };
|
|
6542
|
-
const checked = checkPictures(outcome.drawn);
|
|
7539
|
+
const checked = checkPictures(outcome.drawn, base);
|
|
6543
7540
|
if (!checked.ok) {
|
|
6544
7541
|
return {
|
|
6545
7542
|
stdout: "",
|
|
@@ -6548,33 +7545,34 @@ async function runDraw(repoRoot, deps) {
|
|
|
6548
7545
|
exitCode: 1
|
|
6549
7546
|
};
|
|
6550
7547
|
}
|
|
6551
|
-
const
|
|
7548
|
+
const rel = (which) => diagramRel(base, which);
|
|
7549
|
+
const result = deps.write(repoRoot, base, checked.set);
|
|
6552
7550
|
switch (result.kind) {
|
|
6553
7551
|
case "up-to-date":
|
|
6554
7552
|
return {
|
|
6555
7553
|
stdout: `[halfcycle] The pictures in ${DIAGRAM_DIR_REL}/ are up to date.
|
|
6556
|
-
` + pasteLines(outcome.drawn.mermaid),
|
|
7554
|
+
` + pasteLines(outcome.drawn.mermaid, target),
|
|
6557
7555
|
stderr: warnings,
|
|
6558
7556
|
exitCode: 0
|
|
6559
7557
|
};
|
|
6560
7558
|
case "written":
|
|
6561
7559
|
return {
|
|
6562
|
-
stdout: `[halfcycle] Wrote ${spokenList(DIAGRAM_FILES_AS_LISTED.map(
|
|
6563
|
-
` + pasteLines(outcome.drawn.mermaid),
|
|
7560
|
+
stdout: `[halfcycle] Wrote ${spokenList(DIAGRAM_FILES_AS_LISTED.map(rel))}.
|
|
7561
|
+
` + pasteLines(outcome.drawn.mermaid, target),
|
|
6564
7562
|
stderr: warnings,
|
|
6565
7563
|
exitCode: 0
|
|
6566
7564
|
};
|
|
6567
7565
|
case "in-the-way":
|
|
6568
|
-
return { stdout: "", stderr: warnings + `${P} ${inTheWayLine(result.file, result.temp)}
|
|
7566
|
+
return { stdout: "", stderr: warnings + `${P} ${inTheWayLine(base, result.file, result.temp)}
|
|
6569
7567
|
`, exitCode: 1 };
|
|
6570
7568
|
case "unsafe":
|
|
6571
7569
|
return { stdout: "", stderr: warnings + `${P} ${result.why}
|
|
6572
7570
|
`, exitCode: 1 };
|
|
6573
7571
|
case "failed": {
|
|
6574
|
-
const lost = result.removed.length === 0 ? "The pictures already there are as they were." : `${spokenList(result.removed.map(
|
|
7572
|
+
const lost = result.removed.length === 0 ? "The pictures already there are as they were." : `${spokenList(result.removed.map(rel))} could not be put back as ${result.removed.length === 1 ? "it was, so it was" : "they were, so they were"} removed; the other pictures are as they were.`;
|
|
6575
7573
|
return {
|
|
6576
7574
|
stdout: "",
|
|
6577
|
-
stderr: warnings + `${P} ${
|
|
7575
|
+
stderr: warnings + `${P} ${rel(result.file)} could not be written. ${lost} Check that you can write to ${DIAGRAM_DIR_REL}, then run this again.
|
|
6578
7576
|
`,
|
|
6579
7577
|
exitCode: 1
|
|
6580
7578
|
};
|
|
@@ -6583,13 +7581,14 @@ async function runDraw(repoRoot, deps) {
|
|
|
6583
7581
|
}
|
|
6584
7582
|
|
|
6585
7583
|
// dist/cli-contract.js
|
|
6586
|
-
var CLI_VERBS = ["install", "check-drift", "build-record", "open-phase", "close-phase", "system", "draw", "uninstall"];
|
|
7584
|
+
var CLI_VERBS = ["install", "check-drift", "build-record", "open-phase", "close-phase", "system", "draw", "plan", "uninstall"];
|
|
6587
7585
|
var CONTRACTS = [
|
|
6588
7586
|
{ verb: "install", required: [], conditional: [] },
|
|
6589
7587
|
{ verb: "check-drift", required: [], conditional: [] },
|
|
6590
7588
|
{ verb: "build-record", required: [], conditional: [] },
|
|
6591
7589
|
{ verb: "system", required: [], conditional: [] },
|
|
6592
7590
|
{ verb: "draw", required: [], conditional: [] },
|
|
7591
|
+
{ verb: "plan", required: [], conditional: [] },
|
|
6593
7592
|
{ verb: "uninstall", required: [], conditional: [] },
|
|
6594
7593
|
{
|
|
6595
7594
|
verb: "open-phase",
|
|
@@ -6726,9 +7725,9 @@ function placeholderRefusal(verb, tokens) {
|
|
|
6726
7725
|
}
|
|
6727
7726
|
|
|
6728
7727
|
// dist/uninstall.js
|
|
6729
|
-
import { lstatSync as lstatSync3, readFileSync as
|
|
7728
|
+
import { lstatSync as lstatSync3, readFileSync as readFileSync12, readdirSync, rmSync as rmSync5, rmdirSync, writeFileSync as writeFileSync7 } from "node:fs";
|
|
6730
7729
|
import { homedir as homedir2 } from "node:os";
|
|
6731
|
-
import { dirname as dirname6, join as
|
|
7730
|
+
import { dirname as dirname6, join as join13, relative as relative5, sep as sep2 } from "node:path";
|
|
6732
7731
|
var REMOVE_CREDENTIAL_FLAG = "--remove-credential";
|
|
6733
7732
|
function parseUninstallArgs(args2) {
|
|
6734
7733
|
let removeCredential = false;
|
|
@@ -6754,7 +7753,7 @@ function lexists(path) {
|
|
|
6754
7753
|
function symlinkOnPath(root, rel) {
|
|
6755
7754
|
let current = root;
|
|
6756
7755
|
for (const segment of rel.split("/")) {
|
|
6757
|
-
current =
|
|
7756
|
+
current = join13(current, segment);
|
|
6758
7757
|
try {
|
|
6759
7758
|
if (lstatSync3(current).isSymbolicLink())
|
|
6760
7759
|
return true;
|
|
@@ -6764,7 +7763,7 @@ function symlinkOnPath(root, rel) {
|
|
|
6764
7763
|
}
|
|
6765
7764
|
return false;
|
|
6766
7765
|
}
|
|
6767
|
-
function
|
|
7766
|
+
function isPlainObject3(value) {
|
|
6768
7767
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
6769
7768
|
}
|
|
6770
7769
|
function serializeJson(value) {
|
|
@@ -6776,13 +7775,13 @@ function readMergedJson(targetRepo, rule) {
|
|
|
6776
7775
|
const { rel } = rule;
|
|
6777
7776
|
if (symlinkOnPath(targetRepo, rel))
|
|
6778
7777
|
return { state: "symlink" };
|
|
6779
|
-
const path =
|
|
7778
|
+
const path = join13(targetRepo, rel);
|
|
6780
7779
|
if (!lexists(path))
|
|
6781
7780
|
return { state: "absent" };
|
|
6782
7781
|
let text;
|
|
6783
7782
|
let value;
|
|
6784
7783
|
try {
|
|
6785
|
-
text =
|
|
7784
|
+
text = readFileSync12(path, "utf-8");
|
|
6786
7785
|
} catch {
|
|
6787
7786
|
text = "";
|
|
6788
7787
|
}
|
|
@@ -6791,7 +7790,7 @@ function readMergedJson(targetRepo, rule) {
|
|
|
6791
7790
|
} catch {
|
|
6792
7791
|
value = void 0;
|
|
6793
7792
|
}
|
|
6794
|
-
if (
|
|
7793
|
+
if (isPlainObject3(value))
|
|
6795
7794
|
return { state: "object", text, value };
|
|
6796
7795
|
if (!rule.holdsOurs(text))
|
|
6797
7796
|
return { state: "not-ours" };
|
|
@@ -6799,15 +7798,15 @@ function readMergedJson(targetRepo, rule) {
|
|
|
6799
7798
|
}
|
|
6800
7799
|
function validate(options, rules) {
|
|
6801
7800
|
const { targetRepo, removeCredential, home } = options;
|
|
6802
|
-
const manifestPath =
|
|
6803
|
-
const pinPath =
|
|
7801
|
+
const manifestPath = join13(targetRepo, INSTALL_MANIFEST_REL);
|
|
7802
|
+
const pinPath = join13(targetRepo, BUNDLE_PIN_REL);
|
|
6804
7803
|
if (symlinkOnPath(targetRepo, INSTALL_MANIFEST_REL) || symlinkOnPath(targetRepo, BUNDLE_PIN_REL)) {
|
|
6805
7804
|
throw new Refusal(`.halfcycle, or a file Halfcycle keeps in it, is a symbolic link here, so nothing was followed and nothing was removed.`);
|
|
6806
7805
|
}
|
|
6807
7806
|
let manifest = null;
|
|
6808
7807
|
if (lexists(manifestPath)) {
|
|
6809
7808
|
try {
|
|
6810
|
-
manifest = parseManifest(
|
|
7809
|
+
manifest = parseManifest(readFileSync12(manifestPath, "utf-8"));
|
|
6811
7810
|
} catch (err) {
|
|
6812
7811
|
const why = err instanceof UnreadableManifestError ? err.message : "it could not be read";
|
|
6813
7812
|
throw new Refusal(`${INSTALL_MANIFEST_REL} cannot be read (${why}), so nothing was removed. Restore it from git, or run "${HALFCYCLE_COMMAND}" once to rewrite it, then run "${HALFCYCLE_COMMAND} uninstall" again.`);
|
|
@@ -6817,8 +7816,8 @@ function validate(options, rules) {
|
|
|
6817
7816
|
let pinReadable = true;
|
|
6818
7817
|
if (lexists(pinPath)) {
|
|
6819
7818
|
try {
|
|
6820
|
-
const pin = JSON.parse(
|
|
6821
|
-
const id =
|
|
7819
|
+
const pin = JSON.parse(readFileSync12(pinPath, "utf-8"));
|
|
7820
|
+
const id = isPlainObject3(pin) ? pin[PIN_ENGAGEMENT_ID_FIELD] : void 0;
|
|
6822
7821
|
engagementId = typeof id === "string" && id !== "" ? id : void 0;
|
|
6823
7822
|
} catch {
|
|
6824
7823
|
pinReadable = false;
|
|
@@ -6841,7 +7840,7 @@ function validate(options, rules) {
|
|
|
6841
7840
|
throw new Refusal(`the project id in ${BUNDLE_PIN_REL} is not a valid Halfcycle project id, so it cannot name a folder on this machine. Nothing was removed, here or under ~/.halfcycle.`);
|
|
6842
7841
|
}
|
|
6843
7842
|
if (engagementId !== void 0) {
|
|
6844
|
-
const expectedParent =
|
|
7843
|
+
const expectedParent = join13(home ?? homedir2(), HALFCYCLE_DIR_NAME, ENGAGEMENTS_DIR_NAME);
|
|
6845
7844
|
if (dirname6(engagementStateDir(engagementId, home)) !== expectedParent) {
|
|
6846
7845
|
throw new Refusal(`the project id in ${BUNDLE_PIN_REL} does not name a folder under ~/.halfcycle/engagements. Nothing was removed.`);
|
|
6847
7846
|
}
|
|
@@ -6865,13 +7864,13 @@ function mergedFileRules(writeSet) {
|
|
|
6865
7864
|
function writeBack(targetRepo, rel, current, next, lists, name) {
|
|
6866
7865
|
if (next === current)
|
|
6867
7866
|
return;
|
|
6868
|
-
const path =
|
|
7867
|
+
const path = join13(targetRepo, rel);
|
|
6869
7868
|
try {
|
|
6870
7869
|
if (next === null) {
|
|
6871
7870
|
rmSync5(path);
|
|
6872
7871
|
name(lists.removed, rel, rel);
|
|
6873
7872
|
} else {
|
|
6874
|
-
|
|
7873
|
+
writeFileSync7(path, next, "utf-8");
|
|
6875
7874
|
name(lists.restored, rel, rel);
|
|
6876
7875
|
}
|
|
6877
7876
|
} catch {
|
|
@@ -6883,7 +7882,7 @@ function noRecord(targetRepo, writeSet) {
|
|
|
6883
7882
|
const claim = (rel, header, lines) => {
|
|
6884
7883
|
let text;
|
|
6885
7884
|
try {
|
|
6886
|
-
text = symlinkOnPath(targetRepo, rel) ? "" :
|
|
7885
|
+
text = symlinkOnPath(targetRepo, rel) ? "" : readFileSync12(join13(targetRepo, rel), "utf-8");
|
|
6887
7886
|
} catch {
|
|
6888
7887
|
text = "";
|
|
6889
7888
|
}
|
|
@@ -6912,16 +7911,16 @@ function noRecord(targetRepo, writeSet) {
|
|
|
6912
7911
|
}
|
|
6913
7912
|
function ourHookEntries(value, commands) {
|
|
6914
7913
|
const hooks = value["hooks"];
|
|
6915
|
-
if (!
|
|
7914
|
+
if (!isPlainObject3(hooks))
|
|
6916
7915
|
return [];
|
|
6917
7916
|
const found = [];
|
|
6918
7917
|
for (const [event, entries] of Object.entries(hooks)) {
|
|
6919
7918
|
if (!Array.isArray(entries))
|
|
6920
7919
|
continue;
|
|
6921
7920
|
for (const entry of entries) {
|
|
6922
|
-
if (!
|
|
7921
|
+
if (!isPlainObject3(entry) || !Array.isArray(entry["hooks"]))
|
|
6923
7922
|
continue;
|
|
6924
|
-
const objects = entry["hooks"].filter(
|
|
7923
|
+
const objects = entry["hooks"].filter(isPlainObject3);
|
|
6925
7924
|
if (isHalfcycleEntry({ hooks: objects }, commands)) {
|
|
6926
7925
|
found.push({ event, entry });
|
|
6927
7926
|
}
|
|
@@ -6931,7 +7930,7 @@ function ourHookEntries(value, commands) {
|
|
|
6931
7930
|
}
|
|
6932
7931
|
function ourMcpServers(value) {
|
|
6933
7932
|
const servers = value["mcpServers"];
|
|
6934
|
-
return
|
|
7933
|
+
return isPlainObject3(servers) && MCP_SERVER_KEY in servers ? servers : null;
|
|
6935
7934
|
}
|
|
6936
7935
|
function unmergeSettings(value, rec, writeSet) {
|
|
6937
7936
|
let changed = false;
|
|
@@ -6942,7 +7941,7 @@ function unmergeSettings(value, rec, writeSet) {
|
|
|
6942
7941
|
refused.push(`${SETTINGS_REL}: a deny rule the install record names but Halfcycle never adds (it was kept)`);
|
|
6943
7942
|
}
|
|
6944
7943
|
const hooks = value["hooks"];
|
|
6945
|
-
if (
|
|
7944
|
+
if (isPlainObject3(hooks)) {
|
|
6946
7945
|
const ours = new Set(ourHookEntries(value, commands).map(({ entry }) => entry));
|
|
6947
7946
|
let emptiedAnEvent = false;
|
|
6948
7947
|
for (const event of Object.keys(hooks)) {
|
|
@@ -6977,7 +7976,7 @@ function unmergeSettings(value, rec, writeSet) {
|
|
|
6977
7976
|
}
|
|
6978
7977
|
}
|
|
6979
7978
|
const permissions = value["permissions"];
|
|
6980
|
-
if (
|
|
7979
|
+
if (isPlainObject3(permissions) && Array.isArray(permissions["deny"])) {
|
|
6981
7980
|
const deny = permissions["deny"];
|
|
6982
7981
|
for (const hash of denyHashes) {
|
|
6983
7982
|
for (let i = deny.length - 1; i >= 0; i--) {
|
|
@@ -7009,7 +8008,7 @@ function unmergeSettings(value, rec, writeSet) {
|
|
|
7009
8008
|
}
|
|
7010
8009
|
function unmergeMcp(value, rec) {
|
|
7011
8010
|
const servers = value["mcpServers"];
|
|
7012
|
-
if (!
|
|
8011
|
+
if (!isPlainObject3(servers))
|
|
7013
8012
|
return { changed: false };
|
|
7014
8013
|
let changed = false;
|
|
7015
8014
|
let removedEntry = false;
|
|
@@ -7109,7 +8108,7 @@ function lastOccurrence(text, block) {
|
|
|
7109
8108
|
}
|
|
7110
8109
|
function coveredPathExists(targetRepo, line) {
|
|
7111
8110
|
const path = line.trim().replace(/^\//, "").replace(/\/$/, "");
|
|
7112
|
-
return path !== "" && lexists(
|
|
8111
|
+
return path !== "" && lexists(join13(targetRepo, path));
|
|
7113
8112
|
}
|
|
7114
8113
|
function uninstall(options) {
|
|
7115
8114
|
const result = {
|
|
@@ -7189,7 +8188,7 @@ function uninstall(options) {
|
|
|
7189
8188
|
const gitattributes = manifest.gitattributes ?? { created: false, appended: [] };
|
|
7190
8189
|
trimAppendedFile(targetRepo, gitattributes, deleteWhenEmpty(gitattributes.created), gitattributesRules(writeSet), result, name);
|
|
7191
8190
|
const foundNothing = result.removed.length + result.restored.length + result.kept.length + result.failed.length === 0 && result.keptIgnoreLines.length === 0;
|
|
7192
|
-
if (checked.manifest === null && !lexists(
|
|
8191
|
+
if (checked.manifest === null && !lexists(join13(targetRepo, BUNDLE_PIN_REL)) && foundNothing) {
|
|
7193
8192
|
return { outcome: "not-installed", exitCode: 0, ...result, credential: nothingToSay };
|
|
7194
8193
|
}
|
|
7195
8194
|
const credential = handleCredential(options, checked);
|
|
@@ -7197,7 +8196,7 @@ function uninstall(options) {
|
|
|
7197
8196
|
if (result.failed.length === 0 && !credentialFailed) {
|
|
7198
8197
|
removeInstalledFile(targetRepo, BUNDLE_PIN_REL, recorded.get(BUNDLE_PIN_REL), writeSet, result, name);
|
|
7199
8198
|
try {
|
|
7200
|
-
rmSync5(
|
|
8199
|
+
rmSync5(join13(targetRepo, INSTALL_MANIFEST_REL), { force: true });
|
|
7201
8200
|
named.add(INSTALL_MANIFEST_REL);
|
|
7202
8201
|
} catch {
|
|
7203
8202
|
name(result.failed, INSTALL_MANIFEST_REL, INSTALL_MANIFEST_REL);
|
|
@@ -7232,7 +8231,7 @@ function applyMergedJson(targetRepo, rule, file, deleteWhenEmpty, lists, name, e
|
|
|
7232
8231
|
return;
|
|
7233
8232
|
const empty = Object.keys(value).length === 0 && deleteWhenEmpty;
|
|
7234
8233
|
writeBack(targetRepo, rel, file.text, empty ? null : inLineEndingOf(file.text, serializeJson(value)), lists, name);
|
|
7235
|
-
if (adopted && lexists(
|
|
8234
|
+
if (adopted && lexists(join13(targetRepo, rel))) {
|
|
7236
8235
|
name(lists.leftAlone, `${rel} (Halfcycle's hooks were taken out. There is no record of this file from before the install, so a permission rule or "$schema" Halfcycle added may still be there \u2014 it cannot be told apart from your own)`, rel);
|
|
7237
8236
|
}
|
|
7238
8237
|
}
|
|
@@ -7245,7 +8244,7 @@ function removeInstalledFile(targetRepo, path, entry, writeSet, lists, name) {
|
|
|
7245
8244
|
name(lists.leftAlone, `${path} (a symbolic link, or inside one \u2014 not followed)`, path);
|
|
7246
8245
|
return;
|
|
7247
8246
|
}
|
|
7248
|
-
const abs =
|
|
8247
|
+
const abs = join13(targetRepo, path);
|
|
7249
8248
|
let isFile;
|
|
7250
8249
|
try {
|
|
7251
8250
|
isFile = lstatSync3(abs).isFile();
|
|
@@ -7259,7 +8258,7 @@ function removeInstalledFile(targetRepo, path, entry, writeSet, lists, name) {
|
|
|
7259
8258
|
if (!writeSet.ownedDirs.has(path.slice(0, path.lastIndexOf("/")))) {
|
|
7260
8259
|
let bytes;
|
|
7261
8260
|
try {
|
|
7262
|
-
bytes =
|
|
8261
|
+
bytes = readFileSync12(abs);
|
|
7263
8262
|
} catch {
|
|
7264
8263
|
name(lists.failed, path, path);
|
|
7265
8264
|
return;
|
|
@@ -7279,7 +8278,7 @@ function removeInstalledFile(targetRepo, path, entry, writeSet, lists, name) {
|
|
|
7279
8278
|
function removeEmptyDir(targetRepo, dir, writeSet, lists, name) {
|
|
7280
8279
|
if (!writeSet.dirs.has(dir) || symlinkOnPath(targetRepo, dir))
|
|
7281
8280
|
return;
|
|
7282
|
-
const abs =
|
|
8281
|
+
const abs = join13(targetRepo, dir);
|
|
7283
8282
|
try {
|
|
7284
8283
|
if (!lstatSync3(abs).isDirectory() || readdirSync(abs).length > 0)
|
|
7285
8284
|
return;
|
|
@@ -7298,10 +8297,10 @@ function trimAppendedFile(targetRepo, rec, deleteWhenEmpty, rules, lists, name)
|
|
|
7298
8297
|
name(lists.leftAlone, `${rel} (a symbolic link \u2014 not followed)`, rel);
|
|
7299
8298
|
return [];
|
|
7300
8299
|
}
|
|
7301
|
-
const path =
|
|
8300
|
+
const path = join13(targetRepo, rel);
|
|
7302
8301
|
let before;
|
|
7303
8302
|
try {
|
|
7304
|
-
before =
|
|
8303
|
+
before = readFileSync12(path, "utf-8");
|
|
7305
8304
|
} catch {
|
|
7306
8305
|
return [];
|
|
7307
8306
|
}
|
|
@@ -7345,16 +8344,16 @@ function filesUnder(root, rel) {
|
|
|
7345
8344
|
const walk = (dirRel) => {
|
|
7346
8345
|
let names;
|
|
7347
8346
|
try {
|
|
7348
|
-
if (!lstatSync3(
|
|
8347
|
+
if (!lstatSync3(join13(root, dirRel)).isDirectory())
|
|
7349
8348
|
return;
|
|
7350
|
-
names = readdirSync(
|
|
8349
|
+
names = readdirSync(join13(root, dirRel));
|
|
7351
8350
|
} catch {
|
|
7352
8351
|
return;
|
|
7353
8352
|
}
|
|
7354
8353
|
for (const entry of names.sort()) {
|
|
7355
8354
|
const childRel = `${dirRel}/${entry}`;
|
|
7356
8355
|
try {
|
|
7357
|
-
if (lstatSync3(
|
|
8356
|
+
if (lstatSync3(join13(root, childRel)).isDirectory())
|
|
7358
8357
|
walk(childRel);
|
|
7359
8358
|
else
|
|
7360
8359
|
out.push(childRel);
|
|
@@ -7426,8 +8425,8 @@ function renderUninstallReport(result, home) {
|
|
|
7426
8425
|
}
|
|
7427
8426
|
|
|
7428
8427
|
// dist/banner-facts.js
|
|
7429
|
-
import { readFileSync as
|
|
7430
|
-
import { basename as basename3, join as
|
|
8428
|
+
import { readFileSync as readFileSync13 } from "node:fs";
|
|
8429
|
+
import { basename as basename3, join as join14, resolve as resolve4 } from "node:path";
|
|
7431
8430
|
var BRAND_URL = "halfcycle.ai";
|
|
7432
8431
|
var TAGLINE = [
|
|
7433
8432
|
"Fewer cycles.",
|
|
@@ -7440,7 +8439,7 @@ var TAGLINE_NARROW = [
|
|
|
7440
8439
|
];
|
|
7441
8440
|
function bundleVersion() {
|
|
7442
8441
|
try {
|
|
7443
|
-
const pkg = JSON.parse(
|
|
8442
|
+
const pkg = JSON.parse(readFileSync13(join14(BUNDLE_ROOT, "package.json"), "utf-8"));
|
|
7444
8443
|
return typeof pkg.version === "string" ? pkg.version : void 0;
|
|
7445
8444
|
} catch {
|
|
7446
8445
|
return void 0;
|
|
@@ -7583,7 +8582,9 @@ var USAGE = ` ${HALFCYCLE_COMMAND} [install] [target-repo] [engagement-id] [sel
|
|
|
7583
8582
|
${HALFCYCLE_COMMAND} system [--repo <root>]
|
|
7584
8583
|
check docs/system.json, the description of your system, and send it to Halfcycle
|
|
7585
8584
|
${DRAW_USAGE}
|
|
7586
|
-
draw the architecture
|
|
8585
|
+
draw the architecture picture, or one phase's plan, from docs/system.json into docs/diagrams/
|
|
8586
|
+
${PLAN_USAGE}
|
|
8587
|
+
check a phase's plan in docs/phases/ and send it to Halfcycle; --show lists it for the owner
|
|
7587
8588
|
${HALFCYCLE_COMMAND} uninstall [${REMOVE_CREDENTIAL_FLAG}]
|
|
7588
8589
|
take out what the installer added to this project, keeping anything you changed
|
|
7589
8590
|
${REMOVE_CREDENTIAL_FLAG}: also remove this machine's credential for the project, which every
|
|
@@ -7596,7 +8597,7 @@ var USAGE = ` ${HALFCYCLE_COMMAND} [install] [target-repo] [engagement-id] [sel
|
|
|
7596
8597
|
`;
|
|
7597
8598
|
function isHalfcycleMonorepo(dir) {
|
|
7598
8599
|
try {
|
|
7599
|
-
const pkg = JSON.parse(
|
|
8600
|
+
const pkg = JSON.parse(readFileSync14(join15(dir, "package.json"), "utf-8"));
|
|
7600
8601
|
return pkg.name === "halfcycle-monorepo";
|
|
7601
8602
|
} catch {
|
|
7602
8603
|
return false;
|
|
@@ -8055,12 +9056,38 @@ ${USAGE}`);
|
|
|
8055
9056
|
return;
|
|
8056
9057
|
}
|
|
8057
9058
|
const repoRoot = parsed.repoRoot ?? process.cwd();
|
|
8058
|
-
const run = await runDraw(repoRoot, {
|
|
9059
|
+
const run = await runDraw(repoRoot, parsed.picture, {
|
|
8059
9060
|
readCredential: (root) => readRepoCredential(root),
|
|
8060
9061
|
listTracked: listTrackedFiles,
|
|
8061
9062
|
report: reportSystemDescription,
|
|
9063
|
+
reportPlan: reportPhasePlan,
|
|
8062
9064
|
draw: requestDrawing,
|
|
8063
|
-
write: (root, set) => writeDiagramSet(root, set)
|
|
9065
|
+
write: (root, base, set) => writeDiagramSet(root, base, set)
|
|
9066
|
+
});
|
|
9067
|
+
if (run.stdout !== "")
|
|
9068
|
+
process.stdout.write(run.stdout);
|
|
9069
|
+
if (run.stderr !== "")
|
|
9070
|
+
process.stderr.write(run.stderr);
|
|
9071
|
+
process.exitCode = run.exitCode;
|
|
9072
|
+
return;
|
|
9073
|
+
}
|
|
9074
|
+
if (cmd === "plan") {
|
|
9075
|
+
const parsed = parsePlanArgs(rest);
|
|
9076
|
+
if (!parsed.ok) {
|
|
9077
|
+
process.stderr.write(`halfcycle plan: usage: ${PLAN_USAGE}
|
|
9078
|
+
`);
|
|
9079
|
+
process.exitCode = 2;
|
|
9080
|
+
return;
|
|
9081
|
+
}
|
|
9082
|
+
const repoRoot = parsed.repoRoot ?? process.cwd();
|
|
9083
|
+
const run = await runPlan(repoRoot, parsed.phase, parsed.show, {
|
|
9084
|
+
readCredential: (root) => readRepoCredential(root),
|
|
9085
|
+
listTracked: listTrackedFiles,
|
|
9086
|
+
report: reportSystemDescription,
|
|
9087
|
+
reportPlan: reportPhasePlan,
|
|
9088
|
+
pinnedClientVersion: readPinnedClientVersion,
|
|
9089
|
+
recordShown: (engagementId, entry) => writeShownPlan(engagementId, entry),
|
|
9090
|
+
now: () => /* @__PURE__ */ new Date()
|
|
8064
9091
|
});
|
|
8065
9092
|
if (run.stdout !== "")
|
|
8066
9093
|
process.stdout.write(run.stdout);
|
|
@@ -8113,6 +9140,11 @@ ${USAGE}`);
|
|
|
8113
9140
|
process.stdout.write(`[halfcycle] This decision CROSSED: ${opened.decision.blockingFinding}
|
|
8114
9141
|
`);
|
|
8115
9142
|
}
|
|
9143
|
+
if (opened.methodUpdated) {
|
|
9144
|
+
process.stdout.write(`[halfcycle] ${openMethodMovedLine(opened.methodUpdated)}
|
|
9145
|
+
`);
|
|
9146
|
+
updatePinnedMethodVersion(repoRoot, credential.engagementId, opened.methodUpdated.to);
|
|
9147
|
+
}
|
|
8116
9148
|
if (opened.stampFailure) {
|
|
8117
9149
|
process.stderr.write(`halfcycle open-phase: the phase moved, but this machine's copy of it could not be written: ${opened.stampFailure.reason}
|
|
8118
9150
|
halfcycle open-phase: until ${opened.stampFailure.path} can be written, guard evaluations here are filed under whatever phase that file still names. Fix the file and re-run this command \u2014 re-running it is safe.
|
|
@@ -8230,6 +9262,11 @@ halfcycle open-phase: until ${opened.stampFailure.path} can be written, guard ev
|
|
|
8230
9262
|
process.stdout.write(`[halfcycle] This close was an OVERRIDE: ${closeDecision.reason}
|
|
8231
9263
|
`);
|
|
8232
9264
|
}
|
|
9265
|
+
if (closed.methodUpdated) {
|
|
9266
|
+
process.stdout.write(`[halfcycle] ${closeMethodMovedLine(closed.methodUpdated)}
|
|
9267
|
+
`);
|
|
9268
|
+
updatePinnedMethodVersion(repoRoot, credential.engagementId, closed.methodUpdated.to);
|
|
9269
|
+
}
|
|
8233
9270
|
if (!closed.closeCommit.read) {
|
|
8234
9271
|
process.stderr.write(`halfcycle close-phase: the commit this phase closed at could not be read (${closed.closeCommit.reason}), so its Build Record shows the commit as not stated.
|
|
8235
9272
|
`);
|
|
@@ -8316,6 +9353,12 @@ function readStatementFlags(argv) {
|
|
|
8316
9353
|
function plainOutstandingFinding(finding) {
|
|
8317
9354
|
return finding.replace(/\(attempted-failed\)/g, "(attempted, and did not finish)");
|
|
8318
9355
|
}
|
|
9356
|
+
function closeMethodMovedLine(moved) {
|
|
9357
|
+
return `Halfcycle's method has a newer version, ${moved.to}. Your next phase will follow it. The phase you just closed stays recorded under ${moved.from}, the version it ran under.`;
|
|
9358
|
+
}
|
|
9359
|
+
function openMethodMovedLine(moved) {
|
|
9360
|
+
return `This phase follows Halfcycle's newer method version, ${moved.to}. The project was on ${moved.from}.`;
|
|
9361
|
+
}
|
|
8319
9362
|
function writtenLines(written) {
|
|
8320
9363
|
return `[halfcycle] Build Record written:
|
|
8321
9364
|
${written.mdPath}
|