halfcycle 0.4.6 → 0.4.8
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 +1492 -200
- 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 +1470 -423
- 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 +74 -11
- package/dist/index.js.map +3 -3
- package/dist/install.d.ts +33 -1
- package/dist/install.d.ts.map +1 -1
- package/dist/loopback-signin.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,458 @@ 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/project-page.js
|
|
2071
|
+
function projectPageUrl(engagementId) {
|
|
2072
|
+
return isEngagementId(engagementId) ? `${DASHBOARD_URL}/${encodeURIComponent(engagementId)}` : void 0;
|
|
2073
|
+
}
|
|
2074
|
+
function requestAccessSentence(engagementId) {
|
|
2075
|
+
const page = projectPageUrl(engagementId) ?? "this project's page on Halfcycle";
|
|
2076
|
+
return `To ask for access, open ${page} signed in as this account and choose "Request access". Then let the project's owner know, so they can approve it.`;
|
|
2077
|
+
}
|
|
2078
|
+
|
|
2079
|
+
// ../events/dist/account-identity.js
|
|
2080
|
+
import { z as z12 } from "zod";
|
|
2081
|
+
var accountIdentitySchema = z12.object({
|
|
2082
|
+
accountId: z12.string().min(1),
|
|
2083
|
+
email: z12.string().min(1).optional()
|
|
2084
|
+
}).strict();
|
|
2085
|
+
function safeParseAccountIdentity(payload) {
|
|
2086
|
+
return accountIdentitySchema.safeParse(payload);
|
|
2087
|
+
}
|
|
2088
|
+
|
|
2089
|
+
// ../events/dist/credential-wire.js
|
|
2090
|
+
import { z as z13 } from "zod";
|
|
2091
|
+
var engagementCredentialResponseSchema = z13.object({
|
|
2092
|
+
engagementId: z13.string(),
|
|
2093
|
+
sessionToken: z13.string(),
|
|
2094
|
+
mcpUrl: z13.string(),
|
|
2095
|
+
guardUrl: z13.string(),
|
|
2096
|
+
controlTelemetryUrl: z13.string(),
|
|
2097
|
+
methodVersion: z13.string().optional(),
|
|
2098
|
+
consoleUrl: z13.string().optional()
|
|
2099
|
+
}).strict();
|
|
2100
|
+
var ciTokenExchangeResponseSchema = z13.object({
|
|
2101
|
+
token: z13.string(),
|
|
2102
|
+
expiresAt: utcIso8601,
|
|
2103
|
+
engagementId: z13.string()
|
|
2104
|
+
}).strict();
|
|
2105
|
+
var NO_CREDENTIAL_PLACEHOLDER = "halfcycle:no-credential";
|
|
2106
|
+
var NOT_THIS_ACCOUNT_PLACEHOLDER = "halfcycle:not-this-account";
|
|
2107
|
+
var UPDATE_CLAUDE_CODE_PLACEHOLDER = "halfcycle:update-claude-code";
|
|
2108
|
+
var SWITCH_ACCOUNT_ANSWER = 'answer "s" when asked';
|
|
2109
|
+
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.';
|
|
2110
|
+
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}.`;
|
|
2111
|
+
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.";
|
|
2112
|
+
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.";
|
|
2113
|
+
var PLACEHOLDER_SENTENCES = Object.freeze({
|
|
2114
|
+
[NO_CREDENTIAL_PLACEHOLDER]: NO_CREDENTIAL_SENTENCE,
|
|
2115
|
+
[NOT_THIS_ACCOUNT_PLACEHOLDER]: NOT_THIS_ACCOUNT_SENTENCE,
|
|
2116
|
+
[UPDATE_CLAUDE_CODE_PLACEHOLDER]: UPDATE_CLAUDE_CODE_SENTENCE
|
|
2117
|
+
});
|
|
2118
|
+
|
|
2119
|
+
// ../events/dist/project-engagement.js
|
|
2120
|
+
import { z as z14 } from "zod";
|
|
2121
|
+
var projectIdSchema = z14.string().uuid();
|
|
2122
|
+
var projectEngagementRequestSchema = z14.object({
|
|
2123
|
+
derivedName: z14.string()
|
|
2124
|
+
}).strict();
|
|
2125
|
+
|
|
2126
|
+
// ../events/dist/phase-record.js
|
|
2127
|
+
import { z as z15 } from "zod";
|
|
2128
|
+
var phaseRecordRefusalReasonSchema = z15.enum([
|
|
2129
|
+
"still-open",
|
|
2130
|
+
"reopened",
|
|
2131
|
+
"never-opened",
|
|
2132
|
+
"never-closed",
|
|
2133
|
+
"closed-before-records"
|
|
2134
|
+
]);
|
|
2135
|
+
var phaseRecordRecordSchema = z15.object({
|
|
2136
|
+
outcome: z15.literal("record"),
|
|
2137
|
+
phase: phaseNameSchema,
|
|
2138
|
+
record: buildRecordV2Schema
|
|
2139
|
+
}).strict().refine((body) => body.record.phase === body.phase, {
|
|
2140
|
+
message: "the record is for a different phase than the one asked for",
|
|
2141
|
+
path: ["record", "phase"]
|
|
2142
|
+
});
|
|
2143
|
+
var phaseRecordWithheldSchema = z15.object({
|
|
2144
|
+
outcome: z15.literal("withheld"),
|
|
2145
|
+
phase: phaseNameSchema,
|
|
2146
|
+
sequence: z15.number().int().min(1),
|
|
2147
|
+
closedAt: utcIso8601,
|
|
2148
|
+
message: z15.string()
|
|
2149
|
+
}).strict();
|
|
2150
|
+
var phaseRecordRefusedSchema = wireErrorSchema.extend({
|
|
2151
|
+
outcome: z15.literal("refused"),
|
|
2152
|
+
phase: phaseNameSchema,
|
|
2153
|
+
statusCode: z15.literal(409),
|
|
2154
|
+
error: z15.literal("PhaseRecordRefused"),
|
|
2155
|
+
reason: phaseRecordRefusalReasonSchema
|
|
2156
|
+
}).strict();
|
|
2157
|
+
var phaseRecordResponseSchema = z15.discriminatedUnion("outcome", [
|
|
2158
|
+
phaseRecordRecordSchema,
|
|
2159
|
+
phaseRecordWithheldSchema,
|
|
2160
|
+
phaseRecordRefusedSchema
|
|
2161
|
+
]);
|
|
2162
|
+
|
|
1653
2163
|
// dist/engagement-credential.js
|
|
1654
2164
|
import { chmodSync as chmodSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
1655
2165
|
import { platform as platform2 } from "node:os";
|
|
@@ -2005,17 +2515,17 @@ function fileSha256(bytes) {
|
|
|
2005
2515
|
const text = typeof bytes === "string" ? bytes : Buffer2.from(bytes).toString("utf-8");
|
|
2006
2516
|
return sha256Hex(text.split("\n").map(lineKey).join("\n"));
|
|
2007
2517
|
}
|
|
2008
|
-
function
|
|
2518
|
+
function canonicalJson2(value) {
|
|
2009
2519
|
if (Array.isArray(value))
|
|
2010
|
-
return value.map(
|
|
2520
|
+
return value.map(canonicalJson2);
|
|
2011
2521
|
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,
|
|
2522
|
+
const entries = Object.entries(value).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([k, v]) => [k, canonicalJson2(v)]);
|
|
2013
2523
|
return Object.fromEntries(entries);
|
|
2014
2524
|
}
|
|
2015
2525
|
return value;
|
|
2016
2526
|
}
|
|
2017
2527
|
function canonicalSha256(value) {
|
|
2018
|
-
return sha256Hex(JSON.stringify(
|
|
2528
|
+
return sha256Hex(JSON.stringify(canonicalJson2(value)));
|
|
2019
2529
|
}
|
|
2020
2530
|
function byString(a, b) {
|
|
2021
2531
|
return a < b ? -1 : a > b ? 1 : 0;
|
|
@@ -2034,7 +2544,7 @@ function serializeManifest(manifest) {
|
|
|
2034
2544
|
leftAlone: [...manifest.leftAlone].sort(byString),
|
|
2035
2545
|
settings
|
|
2036
2546
|
};
|
|
2037
|
-
return JSON.stringify(
|
|
2547
|
+
return JSON.stringify(canonicalJson2(ordered), null, 2) + "\n";
|
|
2038
2548
|
}
|
|
2039
2549
|
var UnreadableManifestError = class extends Error {
|
|
2040
2550
|
constructor(reason) {
|
|
@@ -2287,11 +2797,6 @@ function nodeFloorProgram(guarded) {
|
|
|
2287
2797
|
return `if(+process.versions.node.split('.')[0]<${NODE_FLOOR_MAJOR}){console.error('${NODE_FLOOR_SENTENCE_HEAD}'+process.versions.node);process.exitCode=1}else{${guarded}}`;
|
|
2288
2798
|
}
|
|
2289
2799
|
|
|
2290
|
-
// dist/project-page.js
|
|
2291
|
-
function projectPageUrl(engagementId) {
|
|
2292
|
-
return isEngagementId(engagementId) ? `${DASHBOARD_URL}/${encodeURIComponent(engagementId)}` : void 0;
|
|
2293
|
-
}
|
|
2294
|
-
|
|
2295
2800
|
// dist/merge-settings.js
|
|
2296
2801
|
function isHalfcycleCommand(hook, halfcycleCommands) {
|
|
2297
2802
|
if (typeof hook !== "object" || hook === null)
|
|
@@ -2540,7 +3045,7 @@ function recordSettingsMerge(draft, existing, generated, preexisted) {
|
|
|
2540
3045
|
const generatedHooks = generated.hooks ?? {};
|
|
2541
3046
|
const hookCommands = Object.values(generatedHooks).flatMap((entries) => entries.flatMap((entry) => entry.hooks.map((h) => h.command)));
|
|
2542
3047
|
const ours = /* @__PURE__ */ new Set([...hookCommands, ...HALFCYCLE_HOOK_COMMANDS]);
|
|
2543
|
-
const existingHooks =
|
|
3048
|
+
const existingHooks = isPlainObject2(existing.hooks) ? existing.hooks : void 0;
|
|
2544
3049
|
const heldOurs = Object.values(existingHooks ?? {}).some((entries) => Array.isArray(entries) && entries.some((entry) => isHalfcycleEntry(entry, ours)));
|
|
2545
3050
|
const existingDeny = Array.isArray(existing.permissions?.deny) ? existing.permissions.deny : [];
|
|
2546
3051
|
const schemaChanged = generated.$schema !== void 0 && existing.$schema !== generated.$schema;
|
|
@@ -2558,12 +3063,12 @@ function recordSettingsMerge(draft, existing, generated, preexisted) {
|
|
|
2558
3063
|
}
|
|
2559
3064
|
function recordMcpMerge(draft, existingText, writtenText) {
|
|
2560
3065
|
const before = existingText === null ? void 0 : JSON.parse(existingText);
|
|
2561
|
-
const servers =
|
|
3066
|
+
const servers = isPlainObject2(before) ? before["mcpServers"] : void 0;
|
|
2562
3067
|
const written = JSON.parse(writtenText);
|
|
2563
3068
|
recordMcp(draft, {
|
|
2564
3069
|
created: existingText === null,
|
|
2565
|
-
adopted: draft.previous === null &&
|
|
2566
|
-
mcpServersCreated: !(
|
|
3070
|
+
adopted: draft.previous === null && isPlainObject2(servers) && MCP_SERVER_KEY in servers,
|
|
3071
|
+
mcpServersCreated: !(isPlainObject2(before) && "mcpServers" in before),
|
|
2567
3072
|
entrySha256: canonicalSha256(written.mcpServers[MCP_SERVER_KEY])
|
|
2568
3073
|
});
|
|
2569
3074
|
}
|
|
@@ -2576,8 +3081,8 @@ function recordMcpAdoptionOnly(draft, existingText) {
|
|
|
2576
3081
|
} catch {
|
|
2577
3082
|
return;
|
|
2578
3083
|
}
|
|
2579
|
-
const servers =
|
|
2580
|
-
if (!
|
|
3084
|
+
const servers = isPlainObject2(before) ? before["mcpServers"] : void 0;
|
|
3085
|
+
if (!isPlainObject2(servers) || !(MCP_SERVER_KEY in servers))
|
|
2581
3086
|
return;
|
|
2582
3087
|
recordMcp(draft, { created: false, adopted: true, mcpServersCreated: false });
|
|
2583
3088
|
}
|
|
@@ -2662,7 +3167,7 @@ function readFileOrNull(path) {
|
|
|
2662
3167
|
return null;
|
|
2663
3168
|
}
|
|
2664
3169
|
}
|
|
2665
|
-
function
|
|
3170
|
+
function isPlainObject2(value) {
|
|
2666
3171
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
2667
3172
|
}
|
|
2668
3173
|
function readPluginManifest() {
|
|
@@ -3010,6 +3515,54 @@ printf '%s\\n' '${USER_PROMPT_REMINDER_SENTENCE}'
|
|
|
3010
3515
|
exit 0
|
|
3011
3516
|
`;
|
|
3012
3517
|
}
|
|
3518
|
+
var GENERATED_USER_PROMPT_SCOPE_HEADER = "# Typed scope approval \u2014 generated by";
|
|
3519
|
+
function generateUserPromptScopeHook() {
|
|
3520
|
+
return `#!/usr/bin/env bash
|
|
3521
|
+
${GENERATED_USER_PROMPT_SCOPE_HEADER} the Halfcycle installer.
|
|
3522
|
+
#
|
|
3523
|
+
# Lets the owner of this project approve a phase's scope, or send it back, by
|
|
3524
|
+
# typing "approve <phase>" or "send back <phase>: <note>" as the whole prompt in
|
|
3525
|
+
# their own Claude Code session.
|
|
3526
|
+
#
|
|
3527
|
+
# AN ORDINARY PROMPT STARTS NOTHING. This script reads the hook input once and
|
|
3528
|
+
# looks only at the start of the prompt's value. Unless the prompt begins, after
|
|
3529
|
+
# any whitespace (typed, or escaped in the input), with "approve " or
|
|
3530
|
+
# "send back " in any case, it exits 0 with no output, and Node never starts.
|
|
3531
|
+
# A quote inside any other field arrives escaped, so no other field (the folder
|
|
3532
|
+
# the session is in, for one) can read as the start of the prompt.
|
|
3533
|
+
#
|
|
3534
|
+
# Otherwise it finds the project root from its own location, never from the
|
|
3535
|
+
# folder the session has moved to, and hands the same input to the guard
|
|
3536
|
+
# wrapper beside it. That decides whether the line counts and prints at most
|
|
3537
|
+
# one plain line. Nothing here ever blocks a prompt.
|
|
3538
|
+
#
|
|
3539
|
+
# FAILS OPEN up to that hand-over: any error before it exits 0 with no output.
|
|
3540
|
+
# After it, the wrapper's own exit stands.
|
|
3541
|
+
trap 'exit 0' ERR
|
|
3542
|
+
set -e
|
|
3543
|
+
|
|
3544
|
+
# Read with the shell's own builtin, so the check below is the only program an
|
|
3545
|
+
# ordinary prompt starts.
|
|
3546
|
+
HC_INPUT=""
|
|
3547
|
+
IFS= read -r -d '' HC_INPUT || true
|
|
3548
|
+
|
|
3549
|
+
# What may come before the phrase: whitespace as typed, as a short escape, as a
|
|
3550
|
+
# \\u escape, or as the UTF-8 bytes of a Unicode space. Compared byte for byte.
|
|
3551
|
+
HC_LEAD='[[:space:]]|\\\\[nrtf]|\\\\u(000[9a-d]|0020|00a0|1680|200[0-9a]|202[89f]|205f|3000|feff)'
|
|
3552
|
+
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'
|
|
3553
|
+
if ! printf '%s' "$HC_INPUT" | LC_ALL=C grep -Eiq "\\"prompt\\"[[:space:]]*:[[:space:]]*\\"($HC_LEAD)*(approve|send back) "; then
|
|
3554
|
+
exit 0
|
|
3555
|
+
fi
|
|
3556
|
+
|
|
3557
|
+
HC_SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
|
3558
|
+
HC_PROJECT_ROOT=$(CDPATH= cd -- "$HC_SCRIPT_DIR/../.." && pwd)
|
|
3559
|
+
export CLAUDE_PROJECT_DIR="$HC_PROJECT_ROOT"
|
|
3560
|
+
|
|
3561
|
+
trap - ERR
|
|
3562
|
+
set +e
|
|
3563
|
+
exec bash "$HC_PROJECT_ROOT/.claude/hooks/guard-runner.sh" hook user-prompt-submit <<<"$HC_INPUT"
|
|
3564
|
+
`;
|
|
3565
|
+
}
|
|
3013
3566
|
var CI_STANZA_KNOWN_HEADER_HASHES = [
|
|
3014
3567
|
"b225800b2d699811f32ac213238cbf81c35b5e6a74114bdf55e39cd9d7e2928f",
|
|
3015
3568
|
"674181735460f051af7c157222cf851fc3000fb3b8af3a10ea0bde9fe5eaba1f",
|
|
@@ -3085,6 +3638,7 @@ function removeRetiredMcpHeadersHelper(targetRepo) {
|
|
|
3085
3638
|
var OWNED_GENERATED_HEADERS = {
|
|
3086
3639
|
".claude/hooks/guard-runner.sh": GENERATED_GUARD_RUNNER_HEADER,
|
|
3087
3640
|
".claude/hooks/user-prompt-reminder.sh": GENERATED_USER_PROMPT_REMINDER_HEADER,
|
|
3641
|
+
".claude/hooks/user-prompt-scope.sh": GENERATED_USER_PROMPT_SCOPE_HEADER,
|
|
3088
3642
|
".claude/hooks/session-end-marker.sh": GENERATED_SESSION_END_HEADER,
|
|
3089
3643
|
".claude/hooks/session-start-marker.sh": GENERATED_SESSION_START_HEADER
|
|
3090
3644
|
};
|
|
@@ -3093,6 +3647,7 @@ function hookScriptsThisVersionWrites() {
|
|
|
3093
3647
|
[".claude/hooks/guard-runner.sh", generateGuardRunnerWrapper()],
|
|
3094
3648
|
[".claude/hooks/session-end-marker.sh", generateSessionEndMarker()],
|
|
3095
3649
|
[".claude/hooks/user-prompt-reminder.sh", generateUserPromptReminderHook()],
|
|
3650
|
+
[".claude/hooks/user-prompt-scope.sh", generateUserPromptScopeHook()],
|
|
3096
3651
|
[".claude/hooks/session-start-marker.sh", generateSessionStartMarker()]
|
|
3097
3652
|
];
|
|
3098
3653
|
}
|
|
@@ -3237,6 +3792,25 @@ function writeBundlePin(targetRepoRoot, version, engagementId, engagementType, c
|
|
|
3237
3792
|
const pinText = inLineEndingOf(readFileOrNull(pinPath), JSON.stringify(pin, null, 2) + "\n");
|
|
3238
3793
|
writeAllowlisted(pinPath, targetRepoRoot, pinText, writtenPaths);
|
|
3239
3794
|
}
|
|
3795
|
+
function updatePinnedMethodVersion(repoRoot, engagementId, version) {
|
|
3796
|
+
let pin;
|
|
3797
|
+
try {
|
|
3798
|
+
pin = readBundlePin(repoRoot);
|
|
3799
|
+
} catch {
|
|
3800
|
+
return;
|
|
3801
|
+
}
|
|
3802
|
+
if (pin === null || pin.engagementId !== engagementId)
|
|
3803
|
+
return;
|
|
3804
|
+
const pinPath = join5(repoRoot, BUNDLE_PIN_REL);
|
|
3805
|
+
const updated = { ...pin, methodVersion: version };
|
|
3806
|
+
const pinText = inLineEndingOf(readFileOrNull(pinPath), JSON.stringify(updated, null, 2) + "\n");
|
|
3807
|
+
try {
|
|
3808
|
+
writeAllowlisted(pinPath, repoRoot, pinText, []);
|
|
3809
|
+
} catch {
|
|
3810
|
+
process.stderr.write(`[halfcycle] Could not update ${BUNDLE_PIN_REL} with the new method version.
|
|
3811
|
+
`);
|
|
3812
|
+
}
|
|
3813
|
+
}
|
|
3240
3814
|
function writeCrewRoster(targetRepoRoot, report) {
|
|
3241
3815
|
const doc = {
|
|
3242
3816
|
$comment: "GENERATED by the Halfcycle installer \u2014 do not edit by hand. Re-run the installer to pick up a roster change.",
|
|
@@ -4851,10 +5425,10 @@ var SYSTEM_FOLDERS = [
|
|
|
4851
5425
|
"C:\\Program Files",
|
|
4852
5426
|
"C:\\Program Files (x86)"
|
|
4853
5427
|
];
|
|
4854
|
-
function refuseSystemFolder(targetRepo,
|
|
5428
|
+
function refuseSystemFolder(targetRepo, platform5 = process.platform) {
|
|
4855
5429
|
const key = (folder) => {
|
|
4856
5430
|
const real = realOrLiteral(folder);
|
|
4857
|
-
return
|
|
5431
|
+
return platform5 === "win32" ? real.toLowerCase() : real;
|
|
4858
5432
|
};
|
|
4859
5433
|
const target = key(targetRepo);
|
|
4860
5434
|
if (SYSTEM_FOLDERS.some((folder) => key(folder) === target)) {
|
|
@@ -4987,11 +5561,11 @@ import { execFileSync as execFileSync3 } from "node:child_process";
|
|
|
4987
5561
|
import { existsSync as existsSync5, realpathSync as realpathSync2 } from "node:fs";
|
|
4988
5562
|
import { delimiter as delimiter2, join as join7 } from "node:path";
|
|
4989
5563
|
function probeClaudeCodeVersion(timeoutMs, deps = {}) {
|
|
4990
|
-
const
|
|
5564
|
+
const platform5 = deps.platform ?? process.platform;
|
|
4991
5565
|
const exec = deps.exec ?? execFileSync3;
|
|
4992
5566
|
let raw;
|
|
4993
5567
|
try {
|
|
4994
|
-
raw =
|
|
5568
|
+
raw = platform5 === "win32" ? exec("cmd.exe", ["/d", "/s", "/c", '"claude --version"'], {
|
|
4995
5569
|
encoding: "utf-8",
|
|
4996
5570
|
timeout: timeoutMs,
|
|
4997
5571
|
stdio: ["ignore", "pipe", "ignore"],
|
|
@@ -5021,7 +5595,7 @@ var INSTALL_METHOD_RULES = [
|
|
|
5021
5595
|
];
|
|
5022
5596
|
function probeClaudeCodeInstallMethod(deps = {}) {
|
|
5023
5597
|
const env = deps.env ?? process.env;
|
|
5024
|
-
const
|
|
5598
|
+
const platform5 = deps.platform ?? process.platform;
|
|
5025
5599
|
const exists = deps.exists ?? existsSync5;
|
|
5026
5600
|
const realpath = deps.realpath ?? ((file) => {
|
|
5027
5601
|
try {
|
|
@@ -5030,7 +5604,7 @@ function probeClaudeCodeInstallMethod(deps = {}) {
|
|
|
5030
5604
|
return file;
|
|
5031
5605
|
}
|
|
5032
5606
|
});
|
|
5033
|
-
const names =
|
|
5607
|
+
const names = platform5 === "win32" ? ["claude.exe", "claude.cmd"] : ["claude"];
|
|
5034
5608
|
const dirs = (env["PATH"] ?? "").split(delimiter2).filter((dir) => dir !== "");
|
|
5035
5609
|
for (const dir of dirs) {
|
|
5036
5610
|
for (const name of names) {
|
|
@@ -5413,7 +5987,7 @@ function renderBuildRecordV2Markdown(record2) {
|
|
|
5413
5987
|
`Phase: ${phase} (the name the client gave it)`,
|
|
5414
5988
|
OBSERVED_LABEL,
|
|
5415
5989
|
[
|
|
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
|
|
5990
|
+
`- 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
5991
|
`- Project identifier: ${p.text(record2.observed.engagementId)}. Halfcycle keeps this project's records under this identifier.`,
|
|
5418
5992
|
`- 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
5993
|
].join("\n"),
|
|
@@ -5672,6 +6246,10 @@ function refetchCommand(phase) {
|
|
|
5672
6246
|
// dist/open-phase.js
|
|
5673
6247
|
import { readFileSync as readFileSync8 } from "node:fs";
|
|
5674
6248
|
import { join as join9 } from "node:path";
|
|
6249
|
+
function readMethodUpdate(value) {
|
|
6250
|
+
const parsed = methodUpdateSchema.safeParse(value);
|
|
6251
|
+
return parsed.success ? parsed.data : null;
|
|
6252
|
+
}
|
|
5675
6253
|
function describeOpen(opened) {
|
|
5676
6254
|
const { phase, previousPhase } = opened;
|
|
5677
6255
|
if (previousPhase === phase) {
|
|
@@ -5781,13 +6359,18 @@ async function openPhase(credential, phase, entry, home) {
|
|
|
5781
6359
|
phase: body.phase,
|
|
5782
6360
|
previousPhase: body.previousPhase,
|
|
5783
6361
|
stampFailure,
|
|
5784
|
-
decision: body.decision
|
|
6362
|
+
decision: body.decision,
|
|
6363
|
+
methodUpdated: readMethodUpdate(body.methodUpdated)
|
|
5785
6364
|
};
|
|
5786
6365
|
}
|
|
5787
6366
|
|
|
5788
6367
|
// dist/close-phase.js
|
|
5789
6368
|
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
5790
6369
|
var NO_STATEMENTS = { thesis: null, outcome: null, deviations: [] };
|
|
6370
|
+
function readMethodUpdate2(value) {
|
|
6371
|
+
const parsed = methodUpdateSchema.safeParse(value);
|
|
6372
|
+
return parsed.success ? parsed.data : null;
|
|
6373
|
+
}
|
|
5791
6374
|
function readCloseCommit(repoRoot) {
|
|
5792
6375
|
if (repoRoot === void 0) {
|
|
5793
6376
|
return {
|
|
@@ -5876,7 +6459,13 @@ async function closePhase(credential, phase, outcome, close, home, repoRoot, sta
|
|
|
5876
6459
|
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
6460
|
}
|
|
5878
6461
|
const stampFailure = writePhaseStamp(credential.engagementId, null, home);
|
|
5879
|
-
return {
|
|
6462
|
+
return {
|
|
6463
|
+
engagementId: answer.engagementId,
|
|
6464
|
+
status: answer.status,
|
|
6465
|
+
stampFailure,
|
|
6466
|
+
closeCommit,
|
|
6467
|
+
methodUpdated: readMethodUpdate2(answer.methodUpdated)
|
|
6468
|
+
};
|
|
5880
6469
|
}
|
|
5881
6470
|
function describeIssuePath(path) {
|
|
5882
6471
|
const [head, field] = path;
|
|
@@ -5965,8 +6554,8 @@ function unownedPatterns(description, tracked) {
|
|
|
5965
6554
|
}
|
|
5966
6555
|
return found;
|
|
5967
6556
|
}
|
|
5968
|
-
function unownedPatternLines(unowned) {
|
|
5969
|
-
return unowned.map((u) => `halfcycle
|
|
6557
|
+
function unownedPatternLines(unowned, verb) {
|
|
6558
|
+
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
6559
|
`).join("");
|
|
5971
6560
|
}
|
|
5972
6561
|
function systemReportUrl(credential) {
|
|
@@ -6062,8 +6651,8 @@ async function checkAndReportDescription(repoRoot, deps, verb, beforeSend = () =
|
|
|
6062
6651
|
};
|
|
6063
6652
|
}
|
|
6064
6653
|
const unowned = unownedPatterns(checked.description, deps.listTracked(repoRoot));
|
|
6065
|
-
const warnings = unowned === null ? "" : unownedPatternLines(unowned);
|
|
6066
|
-
const refusal = beforeSend();
|
|
6654
|
+
const warnings = unowned === null ? "" : unownedPatternLines(unowned, verb);
|
|
6655
|
+
const refusal = beforeSend(checked.description);
|
|
6067
6656
|
if (refusal !== null)
|
|
6068
6657
|
return { kind: "stopped", stderr: warnings + refusal };
|
|
6069
6658
|
let credential;
|
|
@@ -6116,17 +6705,351 @@ async function runSystem(repoRoot, deps) {
|
|
|
6116
6705
|
}
|
|
6117
6706
|
}
|
|
6118
6707
|
|
|
6708
|
+
// dist/plan.js
|
|
6709
|
+
import { chmodSync as chmodSync3, mkdirSync as mkdirSync7, readFileSync as readFileSync10, renameSync as renameSync2, unlinkSync, writeFileSync as writeFileSync6 } from "node:fs";
|
|
6710
|
+
import { platform as platform4 } from "node:os";
|
|
6711
|
+
import { join as join11 } from "node:path";
|
|
6712
|
+
var PLAN_USAGE = `${HALFCYCLE_COMMAND} plan <phase> [--show] [--repo <root>]`;
|
|
6713
|
+
var PLAN_DIR_REL = "docs/phases";
|
|
6714
|
+
function planFileRel(phase) {
|
|
6715
|
+
return `${PLAN_DIR_REL}/${renderPhaseSegment(phase)}.plan.json`;
|
|
6716
|
+
}
|
|
6717
|
+
var PLAN_CHANGE_LABELS = {
|
|
6718
|
+
add: "Will be added",
|
|
6719
|
+
change: "Will change",
|
|
6720
|
+
remove: "Will be removed"
|
|
6721
|
+
};
|
|
6722
|
+
function parsePlanArgs(argv) {
|
|
6723
|
+
const positionals = [];
|
|
6724
|
+
let repoRoot;
|
|
6725
|
+
let show = false;
|
|
6726
|
+
for (let i = 0; i < argv.length; i++) {
|
|
6727
|
+
const arg = argv[i];
|
|
6728
|
+
if (arg === "--repo") {
|
|
6729
|
+
const value = argv[i + 1];
|
|
6730
|
+
if (repoRoot !== void 0 || value === void 0 || value === "" || value.startsWith("-"))
|
|
6731
|
+
return { ok: false };
|
|
6732
|
+
repoRoot = value;
|
|
6733
|
+
i++;
|
|
6734
|
+
continue;
|
|
6735
|
+
}
|
|
6736
|
+
if (arg === "--show") {
|
|
6737
|
+
if (show)
|
|
6738
|
+
return { ok: false };
|
|
6739
|
+
show = true;
|
|
6740
|
+
continue;
|
|
6741
|
+
}
|
|
6742
|
+
if (arg.startsWith("-"))
|
|
6743
|
+
return { ok: false };
|
|
6744
|
+
positionals.push(arg);
|
|
6745
|
+
}
|
|
6746
|
+
if (positionals.length !== 1)
|
|
6747
|
+
return { ok: false };
|
|
6748
|
+
return { ok: true, phase: positionals[0], show, repoRoot };
|
|
6749
|
+
}
|
|
6750
|
+
function planProblemPlace(raw, fileRel, path) {
|
|
6751
|
+
if (path === "")
|
|
6752
|
+
return fileRel;
|
|
6753
|
+
const [list, index] = path.split(".");
|
|
6754
|
+
const position = Number(index);
|
|
6755
|
+
if (list !== "changes" || !Number.isInteger(position) || position < 0)
|
|
6756
|
+
return path;
|
|
6757
|
+
const changes = typeof raw === "object" && raw !== null ? raw["changes"] : void 0;
|
|
6758
|
+
const item = Array.isArray(changes) ? changes[position] : void 0;
|
|
6759
|
+
const part = typeof item === "object" && item !== null ? item["part"] : void 0;
|
|
6760
|
+
return typeof part === "string" && part !== "" ? `${path} (part "${part}")` : `${path} (part ${position + 1})`;
|
|
6761
|
+
}
|
|
6762
|
+
function planProblemLines(raw, fileRel, problems) {
|
|
6763
|
+
return problems.map((p) => ` ${planProblemPlace(raw, fileRel, p.path)}: ${p.message}
|
|
6764
|
+
`).join("");
|
|
6765
|
+
}
|
|
6766
|
+
function readAndCheckPlan(repoRoot, phase, description, verb) {
|
|
6767
|
+
const p = `halfcycle ${verb}:`;
|
|
6768
|
+
const fileRel = planFileRel(phase);
|
|
6769
|
+
let text;
|
|
6770
|
+
try {
|
|
6771
|
+
text = readFileSync10(join11(repoRoot, fileRel), "utf-8");
|
|
6772
|
+
} catch (err) {
|
|
6773
|
+
const code = err.code;
|
|
6774
|
+
if (code === "ENOENT" || code === "ENOTDIR") {
|
|
6775
|
+
return {
|
|
6776
|
+
ok: false,
|
|
6777
|
+
line: `${p} there is no ${fileRel} in this project yet. Write the plan in the scoping step, then run this again. Nothing was sent.
|
|
6778
|
+
`
|
|
6779
|
+
};
|
|
6780
|
+
}
|
|
6781
|
+
return {
|
|
6782
|
+
ok: false,
|
|
6783
|
+
line: `${p} ${fileRel} could not be read. Check that it is a file you can open, then run this again. Nothing was sent.
|
|
6784
|
+
`
|
|
6785
|
+
};
|
|
6786
|
+
}
|
|
6787
|
+
let raw;
|
|
6788
|
+
try {
|
|
6789
|
+
raw = JSON.parse(text);
|
|
6790
|
+
} catch (err) {
|
|
6791
|
+
const why = err instanceof Error ? err.message : String(err);
|
|
6792
|
+
return { ok: false, line: `${p} ${fileRel} is not valid JSON (${why}). Fix it, then run this again. Nothing was sent.
|
|
6793
|
+
` };
|
|
6794
|
+
}
|
|
6795
|
+
const named = typeof raw === "object" && raw !== null ? raw["phase"] : void 0;
|
|
6796
|
+
if (isValidPhaseIdentity(named) && renderPhaseSegment(named) !== renderPhaseSegment(phase)) {
|
|
6797
|
+
return {
|
|
6798
|
+
ok: false,
|
|
6799
|
+
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.
|
|
6800
|
+
`
|
|
6801
|
+
};
|
|
6802
|
+
}
|
|
6803
|
+
const checked = checkPhasePlan(raw, description);
|
|
6804
|
+
if (!checked.ok) {
|
|
6805
|
+
const n = checked.problems.length;
|
|
6806
|
+
return {
|
|
6807
|
+
ok: false,
|
|
6808
|
+
line: `${p} ${fileRel} has ${n} ${n === 1 ? "problem" : "problems"}, so nothing was sent:
|
|
6809
|
+
` + planProblemLines(raw, fileRel, checked.problems)
|
|
6810
|
+
};
|
|
6811
|
+
}
|
|
6812
|
+
return { ok: true, plan: checked.plan };
|
|
6813
|
+
}
|
|
6814
|
+
function planReportUrl(credential, phase) {
|
|
6815
|
+
return `${credential.serviceUrl}/engagements/${encodeURIComponent(credential.engagementId)}/phases/${encodeURIComponent(String(phase))}/plan`;
|
|
6816
|
+
}
|
|
6817
|
+
function serviceHost2(serviceUrl) {
|
|
6818
|
+
try {
|
|
6819
|
+
return new URL(serviceUrl).host;
|
|
6820
|
+
} catch {
|
|
6821
|
+
return serviceUrl;
|
|
6822
|
+
}
|
|
6823
|
+
}
|
|
6824
|
+
async function reportPhasePlan(credential, plan, descriptionVersion) {
|
|
6825
|
+
const body = { descriptionVersion, plan };
|
|
6826
|
+
let res;
|
|
6827
|
+
let text;
|
|
6828
|
+
try {
|
|
6829
|
+
res = await fetch(planReportUrl(credential, plan.phase), {
|
|
6830
|
+
method: "POST",
|
|
6831
|
+
headers: { authorization: `Bearer ${credential.token}`, "content-type": "application/json" },
|
|
6832
|
+
body: JSON.stringify(body)
|
|
6833
|
+
});
|
|
6834
|
+
text = await res.text();
|
|
6835
|
+
} catch {
|
|
6836
|
+
return {
|
|
6837
|
+
kind: "unavailable",
|
|
6838
|
+
why: `could not reach Halfcycle at ${serviceHost2(credential.serviceUrl)}. Check this machine's connection, then run this again.`
|
|
6839
|
+
};
|
|
6840
|
+
}
|
|
6841
|
+
let raw = null;
|
|
6842
|
+
try {
|
|
6843
|
+
raw = JSON.parse(text);
|
|
6844
|
+
} catch {
|
|
6845
|
+
raw = null;
|
|
6846
|
+
}
|
|
6847
|
+
const parsed = phasePlanResultSchema.safeParse(raw);
|
|
6848
|
+
if (parsed.success) {
|
|
6849
|
+
const answer = parsed.data;
|
|
6850
|
+
if (answer.outcome === "recorded") {
|
|
6851
|
+
return {
|
|
6852
|
+
kind: "recorded",
|
|
6853
|
+
version: answer.version,
|
|
6854
|
+
phaseNumber: answer.phaseNumber,
|
|
6855
|
+
state: answer.state,
|
|
6856
|
+
changedParts: answer.changedParts,
|
|
6857
|
+
typedApprovalOffered: answer.typedApprovalOffered
|
|
6858
|
+
};
|
|
6859
|
+
}
|
|
6860
|
+
if (answer.outcome === "refused")
|
|
6861
|
+
return { kind: "refused", problems: answer.problems };
|
|
6862
|
+
return {
|
|
6863
|
+
kind: "unavailable",
|
|
6864
|
+
why: "Halfcycle could not record the plan just now. Nothing was recorded; run this again in a minute."
|
|
6865
|
+
};
|
|
6866
|
+
}
|
|
6867
|
+
if (isMissingRoute("POST", res.status, raw))
|
|
6868
|
+
return { kind: "no-route" };
|
|
6869
|
+
if (res.ok) {
|
|
6870
|
+
return {
|
|
6871
|
+
kind: "unavailable",
|
|
6872
|
+
why: "Halfcycle sent an answer this command could not read. The plan may not be recorded; run this again later."
|
|
6873
|
+
};
|
|
6874
|
+
}
|
|
6875
|
+
const message = typeof raw === "object" && raw !== null && typeof raw.message === "string" ? raw.message : null;
|
|
6876
|
+
return {
|
|
6877
|
+
kind: "unavailable",
|
|
6878
|
+
why: `Halfcycle answered ${res.status}${message === null ? "." : `: ${message}`}`
|
|
6879
|
+
};
|
|
6880
|
+
}
|
|
6881
|
+
function writeShownPlan(engagementId, entry, home) {
|
|
6882
|
+
let temp = null;
|
|
6883
|
+
try {
|
|
6884
|
+
const dir = engagementStateDir(engagementId, home);
|
|
6885
|
+
const path = join11(dir, SHOWN_PLANS_FILENAME);
|
|
6886
|
+
let existing = [];
|
|
6887
|
+
try {
|
|
6888
|
+
existing = parseShownPlans(readFileSync10(path, "utf-8"));
|
|
6889
|
+
} catch {
|
|
6890
|
+
existing = [];
|
|
6891
|
+
}
|
|
6892
|
+
const kept = existing.filter((e) => e.phase !== entry.phase && e.number !== entry.number);
|
|
6893
|
+
const entries = [...kept, entry];
|
|
6894
|
+
const body = serialiseShownPlans(entries);
|
|
6895
|
+
mkdirSync7(dir, { recursive: true, mode: 448 });
|
|
6896
|
+
temp = join11(dir, `.${SHOWN_PLANS_FILENAME}.${process.pid}.tmp`);
|
|
6897
|
+
writeFileSync6(temp, body, { mode: 384 });
|
|
6898
|
+
if (platform4() !== "win32")
|
|
6899
|
+
chmodSync3(temp, 384);
|
|
6900
|
+
renameSync2(temp, path);
|
|
6901
|
+
return { ok: true, entries };
|
|
6902
|
+
} catch {
|
|
6903
|
+
if (temp !== null) {
|
|
6904
|
+
try {
|
|
6905
|
+
unlinkSync(temp);
|
|
6906
|
+
} catch {
|
|
6907
|
+
}
|
|
6908
|
+
}
|
|
6909
|
+
return { ok: false };
|
|
6910
|
+
}
|
|
6911
|
+
}
|
|
6912
|
+
function readPinnedClientVersion(repoRoot) {
|
|
6913
|
+
try {
|
|
6914
|
+
const version = readBundlePin(repoRoot)?.version;
|
|
6915
|
+
return typeof version === "string" ? version : void 0;
|
|
6916
|
+
} catch {
|
|
6917
|
+
return void 0;
|
|
6918
|
+
}
|
|
6919
|
+
}
|
|
6920
|
+
function planRefusedLines(verb, phase, plan, problems) {
|
|
6921
|
+
const fileRel = planFileRel(phase);
|
|
6922
|
+
const n = problems.length;
|
|
6923
|
+
return `halfcycle ${verb}: Halfcycle refused ${fileRel} (${n} ${n === 1 ? "problem" : "problems"}); the plan was not recorded:
|
|
6924
|
+
` + planProblemLines(plan, fileRel, problems);
|
|
6925
|
+
}
|
|
6926
|
+
function notAPhaseNameLine(verb, phase) {
|
|
6927
|
+
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.
|
|
6928
|
+
`;
|
|
6929
|
+
}
|
|
6930
|
+
function planStateSentence(state) {
|
|
6931
|
+
switch (state) {
|
|
6932
|
+
case "waiting":
|
|
6933
|
+
return "Waiting for the owner's approval.";
|
|
6934
|
+
case "approved":
|
|
6935
|
+
return "The owner has approved this plan.";
|
|
6936
|
+
case "partly-stale":
|
|
6937
|
+
return "The owner approved an earlier plan; the parts marked as changed wait for the owner's approval.";
|
|
6938
|
+
case "sent-back":
|
|
6939
|
+
return "The owner sent this plan back. Change it to answer the owner's notes, then run this again.";
|
|
6940
|
+
case "withdrawn":
|
|
6941
|
+
return "The owner withdrew their approval of this plan. It waits for the owner's approval again.";
|
|
6942
|
+
}
|
|
6943
|
+
}
|
|
6944
|
+
function approvalLine(phase, number, typedApprovalOffered, pinnedVersion, shown2) {
|
|
6945
|
+
if (!typedApprovalOffered)
|
|
6946
|
+
return TYPED_APPROVAL_OFF_SENTENCE;
|
|
6947
|
+
if (!clientMeetsChatApprovalFloor(pinnedVersion))
|
|
6948
|
+
return CLIENT_UPDATE_SENTENCE;
|
|
6949
|
+
const byNumber = String(number);
|
|
6950
|
+
const numberReachesThis = byNumber !== phase && resolvePhaseReference(byNumber, shown2)?.phase === phase;
|
|
6951
|
+
const typed = numberReachesThis ? `\`approve ${phase}\` (or \`approve ${byNumber}\`)` : `\`approve ${phase}\``;
|
|
6952
|
+
return `To approve it, the project's owner types ${typed} in their own Claude Code session. Only the owner can.`;
|
|
6953
|
+
}
|
|
6954
|
+
function changeLines(plan, description, changed) {
|
|
6955
|
+
const names = new Map(description.parts.map((part) => [part.id, part.name]));
|
|
6956
|
+
const marked = new Set(changed);
|
|
6957
|
+
return plan.changes.map((c) => {
|
|
6958
|
+
const mark = marked.has(c.part) ? " (changed since the owner last decided)" : "";
|
|
6959
|
+
return ` - ${names.get(c.part) ?? c.part} \xB7 ${PLAN_CHANGE_LABELS[c.change]} \xB7 ${c.summary}${mark}
|
|
6960
|
+
`;
|
|
6961
|
+
}).join("");
|
|
6962
|
+
}
|
|
6963
|
+
async function runPlan(repoRoot, phase, show, deps) {
|
|
6964
|
+
const p = "halfcycle plan:";
|
|
6965
|
+
if (!isValidPhaseIdentity(phase))
|
|
6966
|
+
return { stdout: "", stderr: notAPhaseNameLine("plan", phase), exitCode: 1 };
|
|
6967
|
+
let checkedPlan = null;
|
|
6968
|
+
let checkedDescription = null;
|
|
6969
|
+
const stage = await checkAndReportDescription(repoRoot, deps, "plan", (description2) => {
|
|
6970
|
+
const checked = readAndCheckPlan(repoRoot, phase, description2, "plan");
|
|
6971
|
+
if (!checked.ok)
|
|
6972
|
+
return checked.line;
|
|
6973
|
+
checkedPlan = checked.plan;
|
|
6974
|
+
checkedDescription = description2;
|
|
6975
|
+
return null;
|
|
6976
|
+
});
|
|
6977
|
+
if (stage.kind === "stopped")
|
|
6978
|
+
return { stdout: "", stderr: stage.stderr, exitCode: 1 };
|
|
6979
|
+
const cannotTakeYet = `${p} this Halfcycle service cannot take a phase plan yet. Nothing was recorded.
|
|
6980
|
+
`;
|
|
6981
|
+
if (stage.kind === "no-route")
|
|
6982
|
+
return { stdout: "", stderr: stage.warnings + cannotTakeYet, exitCode: 1 };
|
|
6983
|
+
const warnings = stage.warnings;
|
|
6984
|
+
const plan = checkedPlan;
|
|
6985
|
+
const description = checkedDescription;
|
|
6986
|
+
if (plan === null || description === null) {
|
|
6987
|
+
return { stdout: "", stderr: warnings + `${p} the plan was not checked, so nothing more was sent.
|
|
6988
|
+
`, exitCode: 1 };
|
|
6989
|
+
}
|
|
6990
|
+
const outcome = await deps.reportPlan(stage.credential, plan, stage.version);
|
|
6991
|
+
switch (outcome.kind) {
|
|
6992
|
+
case "no-route":
|
|
6993
|
+
return { stdout: "", stderr: warnings + cannotTakeYet, exitCode: 1 };
|
|
6994
|
+
case "unavailable":
|
|
6995
|
+
return { stdout: "", stderr: warnings + `${p} ${outcome.why}
|
|
6996
|
+
`, exitCode: 1 };
|
|
6997
|
+
case "refused":
|
|
6998
|
+
return { stdout: "", stderr: warnings + planRefusedLines("plan", phase, plan, outcome.problems), exitCode: 1 };
|
|
6999
|
+
case "recorded":
|
|
7000
|
+
break;
|
|
7001
|
+
}
|
|
7002
|
+
const shownPhase = String(plan.phase);
|
|
7003
|
+
const recorded = `[halfcycle] Plan for ${shownPhase} recorded: plan version ${outcome.version} (phase ${outcome.phaseNumber}).`;
|
|
7004
|
+
const stateSentence = planStateSentence(outcome.state);
|
|
7005
|
+
if (!show)
|
|
7006
|
+
return { stdout: `${recorded} ${stateSentence}
|
|
7007
|
+
`, stderr: warnings, exitCode: 0 };
|
|
7008
|
+
const entry = {
|
|
7009
|
+
phase: shownPhase,
|
|
7010
|
+
number: outcome.phaseNumber,
|
|
7011
|
+
version: outcome.version,
|
|
7012
|
+
shownAt: deps.now().toISOString()
|
|
7013
|
+
};
|
|
7014
|
+
const written = deps.recordShown(stage.credential.engagementId, entry);
|
|
7015
|
+
const listed = `${recorded}
|
|
7016
|
+
` + changeLines(plan, description, outcome.changedParts) + `${stateSentence}
|
|
7017
|
+
`;
|
|
7018
|
+
if (!written.ok) {
|
|
7019
|
+
return {
|
|
7020
|
+
stdout: listed,
|
|
7021
|
+
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.
|
|
7022
|
+
`,
|
|
7023
|
+
exitCode: 1
|
|
7024
|
+
};
|
|
7025
|
+
}
|
|
7026
|
+
const stdout = listed + `${approvalLine(shownPhase, outcome.phaseNumber, outcome.typedApprovalOffered, deps.pinnedClientVersion(repoRoot), written.entries)}
|
|
7027
|
+
`;
|
|
7028
|
+
return { stdout, stderr: warnings, exitCode: 0 };
|
|
7029
|
+
}
|
|
7030
|
+
|
|
6119
7031
|
// dist/draw/write.js
|
|
6120
|
-
import { closeSync as closeSync2, lstatSync as lstatSync2, mkdirSync as
|
|
6121
|
-
import { dirname as dirname5, isAbsolute as isAbsolute3, join as
|
|
7032
|
+
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";
|
|
7033
|
+
import { dirname as dirname5, isAbsolute as isAbsolute3, join as join12, relative as relative4, resolve as resolve3, sep } from "node:path";
|
|
6122
7034
|
var DOCS_DIR = "docs";
|
|
6123
7035
|
var DIAGRAMS_DIR = "diagrams";
|
|
6124
7036
|
var DIAGRAM_DIR_REL = `${DOCS_DIR}/${DIAGRAMS_DIR}`;
|
|
6125
7037
|
var ARCHITECTURE_NAME = "architecture";
|
|
6126
|
-
var
|
|
6127
|
-
|
|
6128
|
-
|
|
6129
|
-
}
|
|
7038
|
+
var ARCHITECTURE_BASE = ARCHITECTURE_NAME;
|
|
7039
|
+
function phaseDiagramBase(phase) {
|
|
7040
|
+
return renderPhaseSegment(phase);
|
|
7041
|
+
}
|
|
7042
|
+
var FLIGHTDECK_SUFFIX = ".flightdeck";
|
|
7043
|
+
function baseTakesFlightDeckNames(base) {
|
|
7044
|
+
return base.toLowerCase().endsWith(FLIGHTDECK_SUFFIX);
|
|
7045
|
+
}
|
|
7046
|
+
function pictureNames(base) {
|
|
7047
|
+
const names = {
|
|
7048
|
+
broadsheet: base,
|
|
7049
|
+
flightdeck: `${base}${FLIGHTDECK_SUFFIX}`
|
|
7050
|
+
};
|
|
7051
|
+
return names;
|
|
7052
|
+
}
|
|
6130
7053
|
var EMBEDDED_LOOK = "broadsheet";
|
|
6131
7054
|
var DIAGRAM_MARKER_LINE = SYSTEM_DIAGRAM_MARKER_LINE;
|
|
6132
7055
|
var PNG_SIGNATURE = Uint8Array.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
@@ -6141,22 +7064,35 @@ var DIAGRAM_FILES = [
|
|
|
6141
7064
|
...SYSTEM_PICTURE_THEMES.map(pngOf),
|
|
6142
7065
|
"mmd"
|
|
6143
7066
|
];
|
|
6144
|
-
|
|
6145
|
-
|
|
6146
|
-
|
|
6147
|
-
|
|
6148
|
-
|
|
6149
|
-
|
|
6150
|
-
])
|
|
6151
|
-
|
|
6152
|
-
|
|
7067
|
+
function fileNames(base) {
|
|
7068
|
+
const looks = pictureNames(base);
|
|
7069
|
+
return Object.fromEntries([
|
|
7070
|
+
...SYSTEM_PICTURE_THEMES.flatMap((look) => [
|
|
7071
|
+
[svgOf(look), `${looks[look]}.svg`],
|
|
7072
|
+
[pngOf(look), `${looks[look]}.png`]
|
|
7073
|
+
]),
|
|
7074
|
+
["mmd", `${base}.mmd`]
|
|
7075
|
+
]);
|
|
7076
|
+
}
|
|
7077
|
+
function embedImageName(base) {
|
|
7078
|
+
return fileNames(base)[pngOf(EMBEDDED_LOOK)];
|
|
7079
|
+
}
|
|
7080
|
+
function markdownLinkName(name) {
|
|
7081
|
+
return encodeURIComponent(name).replace(/\(/g, "%28").replace(/\)/g, "%29");
|
|
7082
|
+
}
|
|
7083
|
+
function embedImageLink(base, doc) {
|
|
7084
|
+
const up = doc === "docs" ? "" : "../";
|
|
7085
|
+
return `${up}${DIAGRAMS_DIR}/${markdownLinkName(embedImageName(base))}`;
|
|
7086
|
+
}
|
|
7087
|
+
function diagramPaths(repoRoot, base) {
|
|
6153
7088
|
const root = resolve3(repoRoot);
|
|
6154
|
-
const dir =
|
|
6155
|
-
const
|
|
7089
|
+
const dir = join12(root, DOCS_DIR, DIAGRAMS_DIR);
|
|
7090
|
+
const names = fileNames(base);
|
|
7091
|
+
const file = Object.fromEntries(DIAGRAM_FILES.map((which) => [which, join12(dir, names[which])]));
|
|
6156
7092
|
return { repoRoot: root, dir, file };
|
|
6157
7093
|
}
|
|
6158
|
-
function diagramRel(which) {
|
|
6159
|
-
return `${DIAGRAM_DIR_REL}/${
|
|
7094
|
+
function diagramRel(base, which) {
|
|
7095
|
+
return `${DIAGRAM_DIR_REL}/${fileNames(base)[which]}`;
|
|
6160
7096
|
}
|
|
6161
7097
|
var DIAGRAM_FILES_AS_LISTED = [
|
|
6162
7098
|
...SYSTEM_PICTURE_THEMES.flatMap((look) => [pngOf(look), svgOf(look)]),
|
|
@@ -6247,7 +7183,7 @@ function readExistingSet(paths) {
|
|
|
6247
7183
|
if (!stat.isFile())
|
|
6248
7184
|
return { ok: false, inTheWay: which, temp: false };
|
|
6249
7185
|
try {
|
|
6250
|
-
found[which] =
|
|
7186
|
+
found[which] = readFileSync11(paths.file[which]);
|
|
6251
7187
|
} catch {
|
|
6252
7188
|
return { ok: false, inTheWay: which, temp: false };
|
|
6253
7189
|
}
|
|
@@ -6293,15 +7229,15 @@ function readExistingSet(paths) {
|
|
|
6293
7229
|
function tempPath(paths, which) {
|
|
6294
7230
|
return `${paths.file[which]}.tmp`;
|
|
6295
7231
|
}
|
|
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.`;
|
|
7232
|
+
function inTheWayLine(base, which, temp = false) {
|
|
7233
|
+
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
7234
|
}
|
|
6299
|
-
function checkPictures(drawn) {
|
|
7235
|
+
function checkPictures(drawn, base) {
|
|
6300
7236
|
const set = {};
|
|
6301
7237
|
for (const look of SYSTEM_PICTURE_THEMES) {
|
|
6302
7238
|
const picture = drawn.pictures[look];
|
|
6303
|
-
const pngFor = diagramRel(pngOf(look));
|
|
6304
|
-
const svgFor = diagramRel(svgOf(look));
|
|
7239
|
+
const pngFor = diagramRel(base, pngOf(look));
|
|
7240
|
+
const svgFor = diagramRel(base, svgOf(look));
|
|
6305
7241
|
const png = Buffer.from(picture.png, "base64");
|
|
6306
7242
|
if (png.length > SYSTEM_DRAW_PNG_MAX_BYTES)
|
|
6307
7243
|
return { ok: false, why: `the image for ${pngFor} is larger than 8 MB` };
|
|
@@ -6337,8 +7273,8 @@ function removeQuietly(path) {
|
|
|
6337
7273
|
function sameBytes(a, b) {
|
|
6338
7274
|
return a !== null && Buffer.from(a).equals(b);
|
|
6339
7275
|
}
|
|
6340
|
-
function writeDiagramSet(repoRoot, set, io = NODE_RECORD_PAIR_IO) {
|
|
6341
|
-
const paths = diagramPaths(repoRoot);
|
|
7276
|
+
function writeDiagramSet(repoRoot, base, set, io = NODE_RECORD_PAIR_IO) {
|
|
7277
|
+
const paths = diagramPaths(repoRoot, base);
|
|
6342
7278
|
const place = checkDiagramPlace(paths);
|
|
6343
7279
|
if (!place.ok)
|
|
6344
7280
|
return { kind: "unsafe", why: place.why };
|
|
@@ -6350,7 +7286,7 @@ function writeDiagramSet(repoRoot, set, io = NODE_RECORD_PAIR_IO) {
|
|
|
6350
7286
|
if (changed.length === 0)
|
|
6351
7287
|
return { kind: "up-to-date" };
|
|
6352
7288
|
try {
|
|
6353
|
-
|
|
7289
|
+
mkdirSync8(paths.dir, { recursive: true });
|
|
6354
7290
|
} catch {
|
|
6355
7291
|
return { kind: "failed", file: changed[0], removed: [] };
|
|
6356
7292
|
}
|
|
@@ -6411,7 +7347,7 @@ function restore(io, paths, renamed, existing) {
|
|
|
6411
7347
|
}
|
|
6412
7348
|
|
|
6413
7349
|
// dist/draw/index.js
|
|
6414
|
-
var DRAW_USAGE = `${HALFCYCLE_COMMAND} draw architecture [--repo <root>]`;
|
|
7350
|
+
var DRAW_USAGE = `${HALFCYCLE_COMMAND} draw (architecture | phase <phase>) [--repo <root>]`;
|
|
6415
7351
|
function parseDrawArgs(argv) {
|
|
6416
7352
|
const positionals = [];
|
|
6417
7353
|
let repoRoot;
|
|
@@ -6429,35 +7365,38 @@ function parseDrawArgs(argv) {
|
|
|
6429
7365
|
return { ok: false };
|
|
6430
7366
|
positionals.push(arg);
|
|
6431
7367
|
}
|
|
6432
|
-
if (positionals.length
|
|
6433
|
-
return { ok:
|
|
6434
|
-
|
|
7368
|
+
if (positionals.length === 1 && positionals[0] === "architecture") {
|
|
7369
|
+
return { ok: true, picture: { kind: "architecture" }, repoRoot };
|
|
7370
|
+
}
|
|
7371
|
+
if (positionals.length === 2 && positionals[0] === "phase") {
|
|
7372
|
+
return { ok: true, picture: { kind: "phase", phase: positionals[1] }, repoRoot };
|
|
7373
|
+
}
|
|
7374
|
+
return { ok: false };
|
|
6435
7375
|
}
|
|
6436
7376
|
function systemDrawUrl(credential) {
|
|
6437
7377
|
return `${credential.serviceUrl}/engagements/${encodeURIComponent(credential.engagementId)}/system/draw`;
|
|
6438
7378
|
}
|
|
6439
|
-
function
|
|
7379
|
+
function serviceHost3(serviceUrl) {
|
|
6440
7380
|
try {
|
|
6441
7381
|
return new URL(serviceUrl).host;
|
|
6442
7382
|
} catch {
|
|
6443
7383
|
return serviceUrl;
|
|
6444
7384
|
}
|
|
6445
7385
|
}
|
|
6446
|
-
async function requestDrawing(credential,
|
|
6447
|
-
const body = { version, picture: "architecture" };
|
|
7386
|
+
async function requestDrawing(credential, request) {
|
|
6448
7387
|
let res;
|
|
6449
7388
|
let text;
|
|
6450
7389
|
try {
|
|
6451
7390
|
res = await fetch(systemDrawUrl(credential), {
|
|
6452
7391
|
method: "POST",
|
|
6453
7392
|
headers: { authorization: `Bearer ${credential.token}`, "content-type": "application/json" },
|
|
6454
|
-
body: JSON.stringify(
|
|
7393
|
+
body: JSON.stringify(request)
|
|
6455
7394
|
});
|
|
6456
7395
|
text = await res.text();
|
|
6457
7396
|
} catch {
|
|
6458
7397
|
return {
|
|
6459
7398
|
kind: "unavailable",
|
|
6460
|
-
why: `could not reach Halfcycle at ${
|
|
7399
|
+
why: `could not reach Halfcycle at ${serviceHost3(credential.serviceUrl)}. Check this machine's connection, then run this again. Nothing was written.`
|
|
6461
7400
|
};
|
|
6462
7401
|
}
|
|
6463
7402
|
let raw = null;
|
|
@@ -6495,39 +7434,101 @@ async function requestDrawing(credential, version) {
|
|
|
6495
7434
|
var P = "halfcycle draw:";
|
|
6496
7435
|
var CANNOT_DRAW_YET = `${P} this Halfcycle service cannot draw pictures yet. Nothing was written.
|
|
6497
7436
|
`;
|
|
6498
|
-
|
|
7437
|
+
var CANNOT_DRAW_PHASE_YET = `${P} this Halfcycle service cannot draw phase pictures yet. Nothing was written.
|
|
7438
|
+
`;
|
|
7439
|
+
function markdownAltText(text) {
|
|
7440
|
+
return text.replace(/[[\]\\]/g, (ch) => `\\${ch}`);
|
|
7441
|
+
}
|
|
7442
|
+
var ARCHITECTURE_PASTE = { doc: "docs", alt: "Architecture", base: ARCHITECTURE_BASE };
|
|
7443
|
+
function phasePaste(phase) {
|
|
7444
|
+
return { doc: "docs/phases", alt: `Phase ${phase}`, base: phaseDiagramBase(phase) };
|
|
7445
|
+
}
|
|
7446
|
+
function pasteLines(mermaid, target) {
|
|
6499
7447
|
const block = mermaid.endsWith("\n") ? mermaid : `${mermaid}
|
|
6500
7448
|
`;
|
|
7449
|
+
const where = target.doc === "docs" ? "a document directly under docs/" : "this phase's document in docs/phases/";
|
|
6501
7450
|
return `
|
|
6502
|
-
Paste this into
|
|
7451
|
+
Paste this into ${where}:
|
|
6503
7452
|
|
|
6504
|
-
})
|
|
6505
7454
|
|
|
6506
7455
|
\`\`\`mermaid
|
|
6507
7456
|
` + block + "```\n";
|
|
6508
7457
|
}
|
|
6509
|
-
function localRefusal(repoRoot) {
|
|
6510
|
-
const paths = diagramPaths(repoRoot);
|
|
7458
|
+
function localRefusal(repoRoot, base) {
|
|
7459
|
+
const paths = diagramPaths(repoRoot, base);
|
|
6511
7460
|
const place = checkDiagramPlace(paths);
|
|
6512
7461
|
if (!place.ok)
|
|
6513
7462
|
return `${P} ${place.why}
|
|
6514
7463
|
`;
|
|
6515
7464
|
const read = readExistingSet(paths);
|
|
6516
7465
|
if (!read.ok)
|
|
6517
|
-
return `${P} ${inTheWayLine(read.inTheWay, read.temp)}
|
|
7466
|
+
return `${P} ${inTheWayLine(base, read.inTheWay, read.temp)}
|
|
6518
7467
|
`;
|
|
6519
7468
|
return null;
|
|
6520
7469
|
}
|
|
6521
|
-
async function runDraw(repoRoot, deps) {
|
|
6522
|
-
|
|
7470
|
+
async function runDraw(repoRoot, picture, deps) {
|
|
7471
|
+
if (picture.kind === "architecture") {
|
|
7472
|
+
const stage2 = await checkAndReportDescription(repoRoot, deps, "draw", () => localRefusal(repoRoot, ARCHITECTURE_BASE));
|
|
7473
|
+
if (stage2.kind === "stopped")
|
|
7474
|
+
return { stdout: "", stderr: stage2.stderr, exitCode: 1 };
|
|
7475
|
+
if (stage2.kind === "no-route")
|
|
7476
|
+
return { stdout: "", stderr: stage2.warnings + CANNOT_DRAW_YET, exitCode: 1 };
|
|
7477
|
+
return drawAndWrite(repoRoot, deps, stage2.credential, { version: stage2.version, picture: "architecture" }, ARCHITECTURE_PASTE, stage2.warnings, CANNOT_DRAW_YET);
|
|
7478
|
+
}
|
|
7479
|
+
const phase = picture.phase;
|
|
7480
|
+
if (!isValidPhaseIdentity(phase))
|
|
7481
|
+
return { stdout: "", stderr: notAPhaseNameLine("draw", phase), exitCode: 1 };
|
|
7482
|
+
const base = phaseDiagramBase(phase);
|
|
7483
|
+
if (baseTakesFlightDeckNames(base)) {
|
|
7484
|
+
return {
|
|
7485
|
+
stdout: "",
|
|
7486
|
+
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.
|
|
7487
|
+
`,
|
|
7488
|
+
exitCode: 1
|
|
7489
|
+
};
|
|
7490
|
+
}
|
|
7491
|
+
let checkedPlan = null;
|
|
7492
|
+
const beforeSend = (description) => {
|
|
7493
|
+
const checked = readAndCheckPlan(repoRoot, phase, description, "draw");
|
|
7494
|
+
if (!checked.ok)
|
|
7495
|
+
return checked.line;
|
|
7496
|
+
const refusal = localRefusal(repoRoot, base);
|
|
7497
|
+
if (refusal !== null)
|
|
7498
|
+
return refusal;
|
|
7499
|
+
checkedPlan = checked.plan;
|
|
7500
|
+
return null;
|
|
7501
|
+
};
|
|
7502
|
+
const stage = await checkAndReportDescription(repoRoot, deps, "draw", beforeSend);
|
|
6523
7503
|
if (stage.kind === "stopped")
|
|
6524
7504
|
return { stdout: "", stderr: stage.stderr, exitCode: 1 };
|
|
6525
7505
|
if (stage.kind === "no-route")
|
|
6526
|
-
return { stdout: "", stderr: stage.warnings +
|
|
7506
|
+
return { stdout: "", stderr: stage.warnings + CANNOT_DRAW_PHASE_YET, exitCode: 1 };
|
|
6527
7507
|
const warnings = stage.warnings;
|
|
6528
|
-
const
|
|
7508
|
+
const plan = checkedPlan;
|
|
7509
|
+
if (plan === null) {
|
|
7510
|
+
return { stdout: "", stderr: warnings + `${P} the plan was not checked, so nothing more was sent.
|
|
7511
|
+
`, exitCode: 1 };
|
|
7512
|
+
}
|
|
7513
|
+
const reported = await deps.reportPlan(stage.credential, plan, stage.version);
|
|
7514
|
+
switch (reported.kind) {
|
|
7515
|
+
case "no-route":
|
|
7516
|
+
return { stdout: "", stderr: warnings + CANNOT_DRAW_PHASE_YET, exitCode: 1 };
|
|
7517
|
+
case "unavailable":
|
|
7518
|
+
return { stdout: "", stderr: warnings + `${P} ${reported.why}
|
|
7519
|
+
`, exitCode: 1 };
|
|
7520
|
+
case "refused":
|
|
7521
|
+
return { stdout: "", stderr: warnings + planRefusedLines("draw", phase, plan, reported.problems), exitCode: 1 };
|
|
7522
|
+
case "recorded":
|
|
7523
|
+
break;
|
|
7524
|
+
}
|
|
7525
|
+
return drawAndWrite(repoRoot, deps, stage.credential, { picture: "phase", phase: plan.phase, planVersion: reported.version }, phasePaste(phase), warnings, CANNOT_DRAW_PHASE_YET);
|
|
7526
|
+
}
|
|
7527
|
+
async function drawAndWrite(repoRoot, deps, credential, request, target, warnings, cannotDrawYet) {
|
|
7528
|
+
const base = target.base;
|
|
7529
|
+
const outcome = await deps.draw(credential, request);
|
|
6529
7530
|
if (outcome.kind === "no-route")
|
|
6530
|
-
return { stdout: "", stderr: warnings +
|
|
7531
|
+
return { stdout: "", stderr: warnings + cannotDrawYet, exitCode: 1 };
|
|
6531
7532
|
if (outcome.kind === "refused") {
|
|
6532
7533
|
return {
|
|
6533
7534
|
stdout: "",
|
|
@@ -6539,7 +7540,7 @@ async function runDraw(repoRoot, deps) {
|
|
|
6539
7540
|
if (outcome.kind === "unavailable")
|
|
6540
7541
|
return { stdout: "", stderr: warnings + `${P} ${outcome.why}
|
|
6541
7542
|
`, exitCode: 1 };
|
|
6542
|
-
const checked = checkPictures(outcome.drawn);
|
|
7543
|
+
const checked = checkPictures(outcome.drawn, base);
|
|
6543
7544
|
if (!checked.ok) {
|
|
6544
7545
|
return {
|
|
6545
7546
|
stdout: "",
|
|
@@ -6548,33 +7549,34 @@ async function runDraw(repoRoot, deps) {
|
|
|
6548
7549
|
exitCode: 1
|
|
6549
7550
|
};
|
|
6550
7551
|
}
|
|
6551
|
-
const
|
|
7552
|
+
const rel = (which) => diagramRel(base, which);
|
|
7553
|
+
const result = deps.write(repoRoot, base, checked.set);
|
|
6552
7554
|
switch (result.kind) {
|
|
6553
7555
|
case "up-to-date":
|
|
6554
7556
|
return {
|
|
6555
7557
|
stdout: `[halfcycle] The pictures in ${DIAGRAM_DIR_REL}/ are up to date.
|
|
6556
|
-
` + pasteLines(outcome.drawn.mermaid),
|
|
7558
|
+
` + pasteLines(outcome.drawn.mermaid, target),
|
|
6557
7559
|
stderr: warnings,
|
|
6558
7560
|
exitCode: 0
|
|
6559
7561
|
};
|
|
6560
7562
|
case "written":
|
|
6561
7563
|
return {
|
|
6562
|
-
stdout: `[halfcycle] Wrote ${spokenList(DIAGRAM_FILES_AS_LISTED.map(
|
|
6563
|
-
` + pasteLines(outcome.drawn.mermaid),
|
|
7564
|
+
stdout: `[halfcycle] Wrote ${spokenList(DIAGRAM_FILES_AS_LISTED.map(rel))}.
|
|
7565
|
+
` + pasteLines(outcome.drawn.mermaid, target),
|
|
6564
7566
|
stderr: warnings,
|
|
6565
7567
|
exitCode: 0
|
|
6566
7568
|
};
|
|
6567
7569
|
case "in-the-way":
|
|
6568
|
-
return { stdout: "", stderr: warnings + `${P} ${inTheWayLine(result.file, result.temp)}
|
|
7570
|
+
return { stdout: "", stderr: warnings + `${P} ${inTheWayLine(base, result.file, result.temp)}
|
|
6569
7571
|
`, exitCode: 1 };
|
|
6570
7572
|
case "unsafe":
|
|
6571
7573
|
return { stdout: "", stderr: warnings + `${P} ${result.why}
|
|
6572
7574
|
`, exitCode: 1 };
|
|
6573
7575
|
case "failed": {
|
|
6574
|
-
const lost = result.removed.length === 0 ? "The pictures already there are as they were." : `${spokenList(result.removed.map(
|
|
7576
|
+
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
7577
|
return {
|
|
6576
7578
|
stdout: "",
|
|
6577
|
-
stderr: warnings + `${P} ${
|
|
7579
|
+
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
7580
|
`,
|
|
6579
7581
|
exitCode: 1
|
|
6580
7582
|
};
|
|
@@ -6583,13 +7585,14 @@ async function runDraw(repoRoot, deps) {
|
|
|
6583
7585
|
}
|
|
6584
7586
|
|
|
6585
7587
|
// dist/cli-contract.js
|
|
6586
|
-
var CLI_VERBS = ["install", "check-drift", "build-record", "open-phase", "close-phase", "system", "draw", "uninstall"];
|
|
7588
|
+
var CLI_VERBS = ["install", "check-drift", "build-record", "open-phase", "close-phase", "system", "draw", "plan", "uninstall"];
|
|
6587
7589
|
var CONTRACTS = [
|
|
6588
7590
|
{ verb: "install", required: [], conditional: [] },
|
|
6589
7591
|
{ verb: "check-drift", required: [], conditional: [] },
|
|
6590
7592
|
{ verb: "build-record", required: [], conditional: [] },
|
|
6591
7593
|
{ verb: "system", required: [], conditional: [] },
|
|
6592
7594
|
{ verb: "draw", required: [], conditional: [] },
|
|
7595
|
+
{ verb: "plan", required: [], conditional: [] },
|
|
6593
7596
|
{ verb: "uninstall", required: [], conditional: [] },
|
|
6594
7597
|
{
|
|
6595
7598
|
verb: "open-phase",
|
|
@@ -6726,9 +7729,9 @@ function placeholderRefusal(verb, tokens) {
|
|
|
6726
7729
|
}
|
|
6727
7730
|
|
|
6728
7731
|
// dist/uninstall.js
|
|
6729
|
-
import { lstatSync as lstatSync3, readFileSync as
|
|
7732
|
+
import { lstatSync as lstatSync3, readFileSync as readFileSync12, readdirSync, rmSync as rmSync5, rmdirSync, writeFileSync as writeFileSync7 } from "node:fs";
|
|
6730
7733
|
import { homedir as homedir2 } from "node:os";
|
|
6731
|
-
import { dirname as dirname6, join as
|
|
7734
|
+
import { dirname as dirname6, join as join13, relative as relative5, sep as sep2 } from "node:path";
|
|
6732
7735
|
var REMOVE_CREDENTIAL_FLAG = "--remove-credential";
|
|
6733
7736
|
function parseUninstallArgs(args2) {
|
|
6734
7737
|
let removeCredential = false;
|
|
@@ -6754,7 +7757,7 @@ function lexists(path) {
|
|
|
6754
7757
|
function symlinkOnPath(root, rel) {
|
|
6755
7758
|
let current = root;
|
|
6756
7759
|
for (const segment of rel.split("/")) {
|
|
6757
|
-
current =
|
|
7760
|
+
current = join13(current, segment);
|
|
6758
7761
|
try {
|
|
6759
7762
|
if (lstatSync3(current).isSymbolicLink())
|
|
6760
7763
|
return true;
|
|
@@ -6764,7 +7767,7 @@ function symlinkOnPath(root, rel) {
|
|
|
6764
7767
|
}
|
|
6765
7768
|
return false;
|
|
6766
7769
|
}
|
|
6767
|
-
function
|
|
7770
|
+
function isPlainObject3(value) {
|
|
6768
7771
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
6769
7772
|
}
|
|
6770
7773
|
function serializeJson(value) {
|
|
@@ -6776,13 +7779,13 @@ function readMergedJson(targetRepo, rule) {
|
|
|
6776
7779
|
const { rel } = rule;
|
|
6777
7780
|
if (symlinkOnPath(targetRepo, rel))
|
|
6778
7781
|
return { state: "symlink" };
|
|
6779
|
-
const path =
|
|
7782
|
+
const path = join13(targetRepo, rel);
|
|
6780
7783
|
if (!lexists(path))
|
|
6781
7784
|
return { state: "absent" };
|
|
6782
7785
|
let text;
|
|
6783
7786
|
let value;
|
|
6784
7787
|
try {
|
|
6785
|
-
text =
|
|
7788
|
+
text = readFileSync12(path, "utf-8");
|
|
6786
7789
|
} catch {
|
|
6787
7790
|
text = "";
|
|
6788
7791
|
}
|
|
@@ -6791,7 +7794,7 @@ function readMergedJson(targetRepo, rule) {
|
|
|
6791
7794
|
} catch {
|
|
6792
7795
|
value = void 0;
|
|
6793
7796
|
}
|
|
6794
|
-
if (
|
|
7797
|
+
if (isPlainObject3(value))
|
|
6795
7798
|
return { state: "object", text, value };
|
|
6796
7799
|
if (!rule.holdsOurs(text))
|
|
6797
7800
|
return { state: "not-ours" };
|
|
@@ -6799,15 +7802,15 @@ function readMergedJson(targetRepo, rule) {
|
|
|
6799
7802
|
}
|
|
6800
7803
|
function validate(options, rules) {
|
|
6801
7804
|
const { targetRepo, removeCredential, home } = options;
|
|
6802
|
-
const manifestPath =
|
|
6803
|
-
const pinPath =
|
|
7805
|
+
const manifestPath = join13(targetRepo, INSTALL_MANIFEST_REL);
|
|
7806
|
+
const pinPath = join13(targetRepo, BUNDLE_PIN_REL);
|
|
6804
7807
|
if (symlinkOnPath(targetRepo, INSTALL_MANIFEST_REL) || symlinkOnPath(targetRepo, BUNDLE_PIN_REL)) {
|
|
6805
7808
|
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
7809
|
}
|
|
6807
7810
|
let manifest = null;
|
|
6808
7811
|
if (lexists(manifestPath)) {
|
|
6809
7812
|
try {
|
|
6810
|
-
manifest = parseManifest(
|
|
7813
|
+
manifest = parseManifest(readFileSync12(manifestPath, "utf-8"));
|
|
6811
7814
|
} catch (err) {
|
|
6812
7815
|
const why = err instanceof UnreadableManifestError ? err.message : "it could not be read";
|
|
6813
7816
|
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 +7820,8 @@ function validate(options, rules) {
|
|
|
6817
7820
|
let pinReadable = true;
|
|
6818
7821
|
if (lexists(pinPath)) {
|
|
6819
7822
|
try {
|
|
6820
|
-
const pin = JSON.parse(
|
|
6821
|
-
const id =
|
|
7823
|
+
const pin = JSON.parse(readFileSync12(pinPath, "utf-8"));
|
|
7824
|
+
const id = isPlainObject3(pin) ? pin[PIN_ENGAGEMENT_ID_FIELD] : void 0;
|
|
6822
7825
|
engagementId = typeof id === "string" && id !== "" ? id : void 0;
|
|
6823
7826
|
} catch {
|
|
6824
7827
|
pinReadable = false;
|
|
@@ -6841,7 +7844,7 @@ function validate(options, rules) {
|
|
|
6841
7844
|
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
7845
|
}
|
|
6843
7846
|
if (engagementId !== void 0) {
|
|
6844
|
-
const expectedParent =
|
|
7847
|
+
const expectedParent = join13(home ?? homedir2(), HALFCYCLE_DIR_NAME, ENGAGEMENTS_DIR_NAME);
|
|
6845
7848
|
if (dirname6(engagementStateDir(engagementId, home)) !== expectedParent) {
|
|
6846
7849
|
throw new Refusal(`the project id in ${BUNDLE_PIN_REL} does not name a folder under ~/.halfcycle/engagements. Nothing was removed.`);
|
|
6847
7850
|
}
|
|
@@ -6865,13 +7868,13 @@ function mergedFileRules(writeSet) {
|
|
|
6865
7868
|
function writeBack(targetRepo, rel, current, next, lists, name) {
|
|
6866
7869
|
if (next === current)
|
|
6867
7870
|
return;
|
|
6868
|
-
const path =
|
|
7871
|
+
const path = join13(targetRepo, rel);
|
|
6869
7872
|
try {
|
|
6870
7873
|
if (next === null) {
|
|
6871
7874
|
rmSync5(path);
|
|
6872
7875
|
name(lists.removed, rel, rel);
|
|
6873
7876
|
} else {
|
|
6874
|
-
|
|
7877
|
+
writeFileSync7(path, next, "utf-8");
|
|
6875
7878
|
name(lists.restored, rel, rel);
|
|
6876
7879
|
}
|
|
6877
7880
|
} catch {
|
|
@@ -6883,7 +7886,7 @@ function noRecord(targetRepo, writeSet) {
|
|
|
6883
7886
|
const claim = (rel, header, lines) => {
|
|
6884
7887
|
let text;
|
|
6885
7888
|
try {
|
|
6886
|
-
text = symlinkOnPath(targetRepo, rel) ? "" :
|
|
7889
|
+
text = symlinkOnPath(targetRepo, rel) ? "" : readFileSync12(join13(targetRepo, rel), "utf-8");
|
|
6887
7890
|
} catch {
|
|
6888
7891
|
text = "";
|
|
6889
7892
|
}
|
|
@@ -6912,16 +7915,16 @@ function noRecord(targetRepo, writeSet) {
|
|
|
6912
7915
|
}
|
|
6913
7916
|
function ourHookEntries(value, commands) {
|
|
6914
7917
|
const hooks = value["hooks"];
|
|
6915
|
-
if (!
|
|
7918
|
+
if (!isPlainObject3(hooks))
|
|
6916
7919
|
return [];
|
|
6917
7920
|
const found = [];
|
|
6918
7921
|
for (const [event, entries] of Object.entries(hooks)) {
|
|
6919
7922
|
if (!Array.isArray(entries))
|
|
6920
7923
|
continue;
|
|
6921
7924
|
for (const entry of entries) {
|
|
6922
|
-
if (!
|
|
7925
|
+
if (!isPlainObject3(entry) || !Array.isArray(entry["hooks"]))
|
|
6923
7926
|
continue;
|
|
6924
|
-
const objects = entry["hooks"].filter(
|
|
7927
|
+
const objects = entry["hooks"].filter(isPlainObject3);
|
|
6925
7928
|
if (isHalfcycleEntry({ hooks: objects }, commands)) {
|
|
6926
7929
|
found.push({ event, entry });
|
|
6927
7930
|
}
|
|
@@ -6931,7 +7934,7 @@ function ourHookEntries(value, commands) {
|
|
|
6931
7934
|
}
|
|
6932
7935
|
function ourMcpServers(value) {
|
|
6933
7936
|
const servers = value["mcpServers"];
|
|
6934
|
-
return
|
|
7937
|
+
return isPlainObject3(servers) && MCP_SERVER_KEY in servers ? servers : null;
|
|
6935
7938
|
}
|
|
6936
7939
|
function unmergeSettings(value, rec, writeSet) {
|
|
6937
7940
|
let changed = false;
|
|
@@ -6942,7 +7945,7 @@ function unmergeSettings(value, rec, writeSet) {
|
|
|
6942
7945
|
refused.push(`${SETTINGS_REL}: a deny rule the install record names but Halfcycle never adds (it was kept)`);
|
|
6943
7946
|
}
|
|
6944
7947
|
const hooks = value["hooks"];
|
|
6945
|
-
if (
|
|
7948
|
+
if (isPlainObject3(hooks)) {
|
|
6946
7949
|
const ours = new Set(ourHookEntries(value, commands).map(({ entry }) => entry));
|
|
6947
7950
|
let emptiedAnEvent = false;
|
|
6948
7951
|
for (const event of Object.keys(hooks)) {
|
|
@@ -6977,7 +7980,7 @@ function unmergeSettings(value, rec, writeSet) {
|
|
|
6977
7980
|
}
|
|
6978
7981
|
}
|
|
6979
7982
|
const permissions = value["permissions"];
|
|
6980
|
-
if (
|
|
7983
|
+
if (isPlainObject3(permissions) && Array.isArray(permissions["deny"])) {
|
|
6981
7984
|
const deny = permissions["deny"];
|
|
6982
7985
|
for (const hash of denyHashes) {
|
|
6983
7986
|
for (let i = deny.length - 1; i >= 0; i--) {
|
|
@@ -7009,7 +8012,7 @@ function unmergeSettings(value, rec, writeSet) {
|
|
|
7009
8012
|
}
|
|
7010
8013
|
function unmergeMcp(value, rec) {
|
|
7011
8014
|
const servers = value["mcpServers"];
|
|
7012
|
-
if (!
|
|
8015
|
+
if (!isPlainObject3(servers))
|
|
7013
8016
|
return { changed: false };
|
|
7014
8017
|
let changed = false;
|
|
7015
8018
|
let removedEntry = false;
|
|
@@ -7109,7 +8112,7 @@ function lastOccurrence(text, block) {
|
|
|
7109
8112
|
}
|
|
7110
8113
|
function coveredPathExists(targetRepo, line) {
|
|
7111
8114
|
const path = line.trim().replace(/^\//, "").replace(/\/$/, "");
|
|
7112
|
-
return path !== "" && lexists(
|
|
8115
|
+
return path !== "" && lexists(join13(targetRepo, path));
|
|
7113
8116
|
}
|
|
7114
8117
|
function uninstall(options) {
|
|
7115
8118
|
const result = {
|
|
@@ -7189,7 +8192,7 @@ function uninstall(options) {
|
|
|
7189
8192
|
const gitattributes = manifest.gitattributes ?? { created: false, appended: [] };
|
|
7190
8193
|
trimAppendedFile(targetRepo, gitattributes, deleteWhenEmpty(gitattributes.created), gitattributesRules(writeSet), result, name);
|
|
7191
8194
|
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(
|
|
8195
|
+
if (checked.manifest === null && !lexists(join13(targetRepo, BUNDLE_PIN_REL)) && foundNothing) {
|
|
7193
8196
|
return { outcome: "not-installed", exitCode: 0, ...result, credential: nothingToSay };
|
|
7194
8197
|
}
|
|
7195
8198
|
const credential = handleCredential(options, checked);
|
|
@@ -7197,7 +8200,7 @@ function uninstall(options) {
|
|
|
7197
8200
|
if (result.failed.length === 0 && !credentialFailed) {
|
|
7198
8201
|
removeInstalledFile(targetRepo, BUNDLE_PIN_REL, recorded.get(BUNDLE_PIN_REL), writeSet, result, name);
|
|
7199
8202
|
try {
|
|
7200
|
-
rmSync5(
|
|
8203
|
+
rmSync5(join13(targetRepo, INSTALL_MANIFEST_REL), { force: true });
|
|
7201
8204
|
named.add(INSTALL_MANIFEST_REL);
|
|
7202
8205
|
} catch {
|
|
7203
8206
|
name(result.failed, INSTALL_MANIFEST_REL, INSTALL_MANIFEST_REL);
|
|
@@ -7232,7 +8235,7 @@ function applyMergedJson(targetRepo, rule, file, deleteWhenEmpty, lists, name, e
|
|
|
7232
8235
|
return;
|
|
7233
8236
|
const empty = Object.keys(value).length === 0 && deleteWhenEmpty;
|
|
7234
8237
|
writeBack(targetRepo, rel, file.text, empty ? null : inLineEndingOf(file.text, serializeJson(value)), lists, name);
|
|
7235
|
-
if (adopted && lexists(
|
|
8238
|
+
if (adopted && lexists(join13(targetRepo, rel))) {
|
|
7236
8239
|
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
8240
|
}
|
|
7238
8241
|
}
|
|
@@ -7245,7 +8248,7 @@ function removeInstalledFile(targetRepo, path, entry, writeSet, lists, name) {
|
|
|
7245
8248
|
name(lists.leftAlone, `${path} (a symbolic link, or inside one \u2014 not followed)`, path);
|
|
7246
8249
|
return;
|
|
7247
8250
|
}
|
|
7248
|
-
const abs =
|
|
8251
|
+
const abs = join13(targetRepo, path);
|
|
7249
8252
|
let isFile;
|
|
7250
8253
|
try {
|
|
7251
8254
|
isFile = lstatSync3(abs).isFile();
|
|
@@ -7259,7 +8262,7 @@ function removeInstalledFile(targetRepo, path, entry, writeSet, lists, name) {
|
|
|
7259
8262
|
if (!writeSet.ownedDirs.has(path.slice(0, path.lastIndexOf("/")))) {
|
|
7260
8263
|
let bytes;
|
|
7261
8264
|
try {
|
|
7262
|
-
bytes =
|
|
8265
|
+
bytes = readFileSync12(abs);
|
|
7263
8266
|
} catch {
|
|
7264
8267
|
name(lists.failed, path, path);
|
|
7265
8268
|
return;
|
|
@@ -7279,7 +8282,7 @@ function removeInstalledFile(targetRepo, path, entry, writeSet, lists, name) {
|
|
|
7279
8282
|
function removeEmptyDir(targetRepo, dir, writeSet, lists, name) {
|
|
7280
8283
|
if (!writeSet.dirs.has(dir) || symlinkOnPath(targetRepo, dir))
|
|
7281
8284
|
return;
|
|
7282
|
-
const abs =
|
|
8285
|
+
const abs = join13(targetRepo, dir);
|
|
7283
8286
|
try {
|
|
7284
8287
|
if (!lstatSync3(abs).isDirectory() || readdirSync(abs).length > 0)
|
|
7285
8288
|
return;
|
|
@@ -7298,10 +8301,10 @@ function trimAppendedFile(targetRepo, rec, deleteWhenEmpty, rules, lists, name)
|
|
|
7298
8301
|
name(lists.leftAlone, `${rel} (a symbolic link \u2014 not followed)`, rel);
|
|
7299
8302
|
return [];
|
|
7300
8303
|
}
|
|
7301
|
-
const path =
|
|
8304
|
+
const path = join13(targetRepo, rel);
|
|
7302
8305
|
let before;
|
|
7303
8306
|
try {
|
|
7304
|
-
before =
|
|
8307
|
+
before = readFileSync12(path, "utf-8");
|
|
7305
8308
|
} catch {
|
|
7306
8309
|
return [];
|
|
7307
8310
|
}
|
|
@@ -7345,16 +8348,16 @@ function filesUnder(root, rel) {
|
|
|
7345
8348
|
const walk = (dirRel) => {
|
|
7346
8349
|
let names;
|
|
7347
8350
|
try {
|
|
7348
|
-
if (!lstatSync3(
|
|
8351
|
+
if (!lstatSync3(join13(root, dirRel)).isDirectory())
|
|
7349
8352
|
return;
|
|
7350
|
-
names = readdirSync(
|
|
8353
|
+
names = readdirSync(join13(root, dirRel));
|
|
7351
8354
|
} catch {
|
|
7352
8355
|
return;
|
|
7353
8356
|
}
|
|
7354
8357
|
for (const entry of names.sort()) {
|
|
7355
8358
|
const childRel = `${dirRel}/${entry}`;
|
|
7356
8359
|
try {
|
|
7357
|
-
if (lstatSync3(
|
|
8360
|
+
if (lstatSync3(join13(root, childRel)).isDirectory())
|
|
7358
8361
|
walk(childRel);
|
|
7359
8362
|
else
|
|
7360
8363
|
out.push(childRel);
|
|
@@ -7426,8 +8429,8 @@ function renderUninstallReport(result, home) {
|
|
|
7426
8429
|
}
|
|
7427
8430
|
|
|
7428
8431
|
// dist/banner-facts.js
|
|
7429
|
-
import { readFileSync as
|
|
7430
|
-
import { basename as basename3, join as
|
|
8432
|
+
import { readFileSync as readFileSync13 } from "node:fs";
|
|
8433
|
+
import { basename as basename3, join as join14, resolve as resolve4 } from "node:path";
|
|
7431
8434
|
var BRAND_URL = "halfcycle.ai";
|
|
7432
8435
|
var TAGLINE = [
|
|
7433
8436
|
"Fewer cycles.",
|
|
@@ -7440,7 +8443,7 @@ var TAGLINE_NARROW = [
|
|
|
7440
8443
|
];
|
|
7441
8444
|
function bundleVersion() {
|
|
7442
8445
|
try {
|
|
7443
|
-
const pkg = JSON.parse(
|
|
8446
|
+
const pkg = JSON.parse(readFileSync13(join14(BUNDLE_ROOT, "package.json"), "utf-8"));
|
|
7444
8447
|
return typeof pkg.version === "string" ? pkg.version : void 0;
|
|
7445
8448
|
} catch {
|
|
7446
8449
|
return void 0;
|
|
@@ -7583,7 +8586,9 @@ var USAGE = ` ${HALFCYCLE_COMMAND} [install] [target-repo] [engagement-id] [sel
|
|
|
7583
8586
|
${HALFCYCLE_COMMAND} system [--repo <root>]
|
|
7584
8587
|
check docs/system.json, the description of your system, and send it to Halfcycle
|
|
7585
8588
|
${DRAW_USAGE}
|
|
7586
|
-
draw the architecture
|
|
8589
|
+
draw the architecture picture, or one phase's plan, from docs/system.json into docs/diagrams/
|
|
8590
|
+
${PLAN_USAGE}
|
|
8591
|
+
check a phase's plan in docs/phases/ and send it to Halfcycle; --show lists it for the owner
|
|
7587
8592
|
${HALFCYCLE_COMMAND} uninstall [${REMOVE_CREDENTIAL_FLAG}]
|
|
7588
8593
|
take out what the installer added to this project, keeping anything you changed
|
|
7589
8594
|
${REMOVE_CREDENTIAL_FLAG}: also remove this machine's credential for the project, which every
|
|
@@ -7596,7 +8601,7 @@ var USAGE = ` ${HALFCYCLE_COMMAND} [install] [target-repo] [engagement-id] [sel
|
|
|
7596
8601
|
`;
|
|
7597
8602
|
function isHalfcycleMonorepo(dir) {
|
|
7598
8603
|
try {
|
|
7599
|
-
const pkg = JSON.parse(
|
|
8604
|
+
const pkg = JSON.parse(readFileSync14(join15(dir, "package.json"), "utf-8"));
|
|
7600
8605
|
return pkg.name === "halfcycle-monorepo";
|
|
7601
8606
|
} catch {
|
|
7602
8607
|
return false;
|
|
@@ -7885,7 +8890,7 @@ ${PROJECT_RESET_LINE}
|
|
|
7885
8890
|
`);
|
|
7886
8891
|
}
|
|
7887
8892
|
if (credential === void 0 && notThisAccountMessage !== void 0) {
|
|
7888
|
-
process.stdout.write(`[halfcycle] ${notThisAccountMessage} Your work carries on exactly as it would without Halfcycle: no edit is evaluated, and nothing you do here is reported as having passed.
|
|
8893
|
+
process.stdout.write(`[halfcycle] ${notThisAccountMessage} Your work carries on exactly as it would without Halfcycle: no edit is evaluated, and nothing you do here is reported as having passed. ${requestAccessSentence(engagementId)} ${identitySwitchRemedy(void 0, isInteractiveRun())}
|
|
7889
8894
|
`);
|
|
7890
8895
|
}
|
|
7891
8896
|
reportClaudeCodeVersion(verbose);
|
|
@@ -8055,12 +9060,38 @@ ${USAGE}`);
|
|
|
8055
9060
|
return;
|
|
8056
9061
|
}
|
|
8057
9062
|
const repoRoot = parsed.repoRoot ?? process.cwd();
|
|
8058
|
-
const run = await runDraw(repoRoot, {
|
|
9063
|
+
const run = await runDraw(repoRoot, parsed.picture, {
|
|
8059
9064
|
readCredential: (root) => readRepoCredential(root),
|
|
8060
9065
|
listTracked: listTrackedFiles,
|
|
8061
9066
|
report: reportSystemDescription,
|
|
9067
|
+
reportPlan: reportPhasePlan,
|
|
8062
9068
|
draw: requestDrawing,
|
|
8063
|
-
write: (root, set) => writeDiagramSet(root, set)
|
|
9069
|
+
write: (root, base, set) => writeDiagramSet(root, base, set)
|
|
9070
|
+
});
|
|
9071
|
+
if (run.stdout !== "")
|
|
9072
|
+
process.stdout.write(run.stdout);
|
|
9073
|
+
if (run.stderr !== "")
|
|
9074
|
+
process.stderr.write(run.stderr);
|
|
9075
|
+
process.exitCode = run.exitCode;
|
|
9076
|
+
return;
|
|
9077
|
+
}
|
|
9078
|
+
if (cmd === "plan") {
|
|
9079
|
+
const parsed = parsePlanArgs(rest);
|
|
9080
|
+
if (!parsed.ok) {
|
|
9081
|
+
process.stderr.write(`halfcycle plan: usage: ${PLAN_USAGE}
|
|
9082
|
+
`);
|
|
9083
|
+
process.exitCode = 2;
|
|
9084
|
+
return;
|
|
9085
|
+
}
|
|
9086
|
+
const repoRoot = parsed.repoRoot ?? process.cwd();
|
|
9087
|
+
const run = await runPlan(repoRoot, parsed.phase, parsed.show, {
|
|
9088
|
+
readCredential: (root) => readRepoCredential(root),
|
|
9089
|
+
listTracked: listTrackedFiles,
|
|
9090
|
+
report: reportSystemDescription,
|
|
9091
|
+
reportPlan: reportPhasePlan,
|
|
9092
|
+
pinnedClientVersion: readPinnedClientVersion,
|
|
9093
|
+
recordShown: (engagementId, entry) => writeShownPlan(engagementId, entry),
|
|
9094
|
+
now: () => /* @__PURE__ */ new Date()
|
|
8064
9095
|
});
|
|
8065
9096
|
if (run.stdout !== "")
|
|
8066
9097
|
process.stdout.write(run.stdout);
|
|
@@ -8113,6 +9144,11 @@ ${USAGE}`);
|
|
|
8113
9144
|
process.stdout.write(`[halfcycle] This decision CROSSED: ${opened.decision.blockingFinding}
|
|
8114
9145
|
`);
|
|
8115
9146
|
}
|
|
9147
|
+
if (opened.methodUpdated) {
|
|
9148
|
+
process.stdout.write(`[halfcycle] ${openMethodMovedLine(opened.methodUpdated)}
|
|
9149
|
+
`);
|
|
9150
|
+
updatePinnedMethodVersion(repoRoot, credential.engagementId, opened.methodUpdated.to);
|
|
9151
|
+
}
|
|
8116
9152
|
if (opened.stampFailure) {
|
|
8117
9153
|
process.stderr.write(`halfcycle open-phase: the phase moved, but this machine's copy of it could not be written: ${opened.stampFailure.reason}
|
|
8118
9154
|
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 +9266,11 @@ halfcycle open-phase: until ${opened.stampFailure.path} can be written, guard ev
|
|
|
8230
9266
|
process.stdout.write(`[halfcycle] This close was an OVERRIDE: ${closeDecision.reason}
|
|
8231
9267
|
`);
|
|
8232
9268
|
}
|
|
9269
|
+
if (closed.methodUpdated) {
|
|
9270
|
+
process.stdout.write(`[halfcycle] ${closeMethodMovedLine(closed.methodUpdated)}
|
|
9271
|
+
`);
|
|
9272
|
+
updatePinnedMethodVersion(repoRoot, credential.engagementId, closed.methodUpdated.to);
|
|
9273
|
+
}
|
|
8233
9274
|
if (!closed.closeCommit.read) {
|
|
8234
9275
|
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
9276
|
`);
|
|
@@ -8316,6 +9357,12 @@ function readStatementFlags(argv) {
|
|
|
8316
9357
|
function plainOutstandingFinding(finding) {
|
|
8317
9358
|
return finding.replace(/\(attempted-failed\)/g, "(attempted, and did not finish)");
|
|
8318
9359
|
}
|
|
9360
|
+
function closeMethodMovedLine(moved) {
|
|
9361
|
+
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.`;
|
|
9362
|
+
}
|
|
9363
|
+
function openMethodMovedLine(moved) {
|
|
9364
|
+
return `This phase follows Halfcycle's newer method version, ${moved.to}. The project was on ${moved.from}.`;
|
|
9365
|
+
}
|
|
8319
9366
|
function writtenLines(written) {
|
|
8320
9367
|
return `[halfcycle] Build Record written:
|
|
8321
9368
|
${written.mdPath}
|