@cassiomc1/forgeloop 0.1.6 → 0.1.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/AGENT_COMPATIBILITY.md +28 -16
- package/ENG/taste-frontend-eng.md +139 -0
- package/EXECUTION_STATE.md +12 -5
- package/GUIDE_ROUTER.md +24 -1
- package/LOOP_ENGINEERING.md +43 -0
- package/LOOP_SYSTEM_DESIGN.md +50 -49
- package/PROJECT_PROFILE.md +27 -12
- package/QUALITY_SCORECARD.md +7 -0
- package/README.md +141 -75
- package/THIRD_PARTY_NOTICES.md +11 -0
- package/conformance/README.md +7 -1
- package/conformance/blind-premium-website/EXPECTED_ROUTE.json +1 -1
- package/conformance/complete-website/EXPECTED_ROUTE.json +1 -1
- package/conformance/runs/2026-08-13-codex-sixth-live.md +412 -0
- package/package.json +1 -1
- package/src/cli.js +2 -1
- package/src/commands/doctor.js +110 -13
- package/src/commands/init.js +12 -5
- package/src/commands/update.js +215 -2
- package/src/commands/validate-protocol.js +19 -2
- package/src/core/audit.js +16 -1
- package/src/core/events.js +8 -0
- package/src/core/guide-metadata.js +1 -0
- package/src/core/inspect.js +16 -7
- package/src/core/manifest.js +6 -0
- package/src/core/native-adapters.js +74 -0
- package/src/core/next-action.js +42 -1
- package/src/core/preflight.js +220 -46
- package/src/core/profile.js +12 -4
- package/src/core/protocol.js +8 -0
- package/src/core/report.js +4 -1
- package/src/core/resumability.js +61 -0
- package/src/core/route-artifact.js +9 -2
- package/src/core/router.js +2 -1
- package/src/core/target-layout.js +38 -0
- package/src/core/templates.js +14 -2
package/src/core/preflight.js
CHANGED
|
@@ -5,13 +5,15 @@ import { requiredGatesForGuides } from "./guide-metadata.js";
|
|
|
5
5
|
import { assertRouteInvariants } from "./router.js";
|
|
6
6
|
import { assertSourceProvenance } from "./sources.js";
|
|
7
7
|
import { readPersistedRoute } from "./route-artifact.js";
|
|
8
|
-
import { appendProtocolEvent,
|
|
8
|
+
import { appendProtocolEvent, validateEventLedger } from "./events.js";
|
|
9
9
|
import { readWorkState } from "./work-state.js";
|
|
10
10
|
import { readConfig } from "./config.js";
|
|
11
11
|
import { assertSafePath, ensureWithin, fileExists, readBytes } from "./filesystem.js";
|
|
12
12
|
import { sha256 } from "./manifest.js";
|
|
13
|
-
import { validateProfileSources } from "./profile.js";
|
|
13
|
+
import { findProfilePath, validateProfileSources } from "./profile.js";
|
|
14
14
|
import { assertStateIdentity } from "./completion-relationships.js";
|
|
15
|
+
import { ensureResumableState, synchronizePreflightState } from "./resumability.js";
|
|
16
|
+
import { PROFILE_PATH } from "./target-layout.js";
|
|
15
17
|
|
|
16
18
|
const PREVIEW_DECISION_LIMIT = 10;
|
|
17
19
|
const PREVIEW_DECISION_MAX_LENGTH = 240;
|
|
@@ -91,10 +93,9 @@ export function validatePersistedPreflight(persisted, current) {
|
|
|
91
93
|
}
|
|
92
94
|
|
|
93
95
|
async function readProfile(target) {
|
|
94
|
-
const relativePath =
|
|
95
|
-
|
|
96
|
+
const relativePath = await findProfilePath(target);
|
|
97
|
+
if (!relativePath) return { status: "missing", fingerprint: null };
|
|
96
98
|
const filePath = ensureWithin(target, relativePath);
|
|
97
|
-
if (!(await fileExists(filePath))) return { status: "missing", fingerprint: null };
|
|
98
99
|
const bytes = await readBytes(filePath);
|
|
99
100
|
const text = bytes.toString("utf8");
|
|
100
101
|
const mode = text.match(/^profile-mode:\s*([^\s]+)\s*$/m)?.[1] ?? null;
|
|
@@ -204,7 +205,7 @@ export async function evaluatePreflight({ target, packageRoot, strict = false }
|
|
|
204
205
|
const config = await optionalConfig(target, packageRoot, errors);
|
|
205
206
|
const effectiveStrict = strict || config.complianceMode === "strict";
|
|
206
207
|
if (effectiveStrict && profile.status !== "verified") {
|
|
207
|
-
errors.push(issue("E_PROFILE_UNVERIFIED", "Strict preflight requires a verified project profile", [
|
|
208
|
+
errors.push(issue("E_PROFILE_UNVERIFIED", "Strict preflight requires a verified project profile", [PROFILE_PATH]));
|
|
208
209
|
}
|
|
209
210
|
const unresolvedDecisions = contract?.value?.unresolvedDecisions ?? [];
|
|
210
211
|
if (unresolvedDecisions.length > 0) {
|
|
@@ -348,62 +349,235 @@ function sameReadyPreflightEvent(event, result) {
|
|
|
348
349
|
&& sameStringSet(event.details?.satisfiedGates, result.satisfiedGates);
|
|
349
350
|
}
|
|
350
351
|
|
|
351
|
-
function
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
352
|
+
export async function validateReadyProtocolConsistency({
|
|
353
|
+
target,
|
|
354
|
+
packageRoot,
|
|
355
|
+
persisted,
|
|
356
|
+
current = null,
|
|
357
|
+
} = {}) {
|
|
358
|
+
if (persisted?.status !== "READY") return [];
|
|
359
|
+
const result = current ?? await evaluatePreflight({ target, packageRoot });
|
|
360
|
+
const errors = [...validatePersistedPreflight(persisted, result)];
|
|
361
|
+
|
|
362
|
+
let state = null;
|
|
363
|
+
try {
|
|
364
|
+
state = await readWorkState(target, packageRoot);
|
|
365
|
+
} catch (error) {
|
|
366
|
+
errors.push(issue("E_STATE_INVALID", error.message, [ARTIFACT_PATHS.state]));
|
|
367
|
+
}
|
|
368
|
+
if (!state) {
|
|
369
|
+
errors.push(issue(
|
|
370
|
+
"E_STATE_MISSING_AFTER_PREFLIGHT_READY",
|
|
371
|
+
"A persisted READY preflight must have a resumable work-state checkpoint",
|
|
372
|
+
[ARTIFACT_PATHS.preflight, ARTIFACT_PATHS.state],
|
|
373
|
+
));
|
|
374
|
+
} else {
|
|
375
|
+
if (state.taskId !== persisted.taskId) {
|
|
376
|
+
errors.push(issue(
|
|
377
|
+
"E_STATE_TASK_MISMATCH",
|
|
378
|
+
"The resumable checkpoint does not belong to the READY preflight task",
|
|
379
|
+
[ARTIFACT_PATHS.state, ARTIFACT_PATHS.preflight],
|
|
380
|
+
));
|
|
381
|
+
}
|
|
382
|
+
if (state.contractFingerprint !== result.fingerprints.contract) {
|
|
383
|
+
errors.push(issue(
|
|
384
|
+
"E_CONTRACT_STALE",
|
|
385
|
+
"The resumable checkpoint does not match the READY contract fingerprint",
|
|
386
|
+
[ARTIFACT_PATHS.state, ARTIFACT_PATHS.contract],
|
|
387
|
+
));
|
|
388
|
+
}
|
|
389
|
+
if (state.routeFingerprint !== result.fingerprints.routing) {
|
|
390
|
+
errors.push(issue(
|
|
391
|
+
"E_ROUTE_STALE",
|
|
392
|
+
"The resumable checkpoint does not match the READY routing fingerprint",
|
|
393
|
+
[ARTIFACT_PATHS.state, ARTIFACT_PATHS.route],
|
|
394
|
+
));
|
|
395
|
+
}
|
|
396
|
+
if (JSON.stringify(state.selectedGuides) !== JSON.stringify(result.routing.guides)) {
|
|
397
|
+
errors.push(issue(
|
|
398
|
+
"E_ROUTE_GUIDE_MISMATCH",
|
|
399
|
+
"The resumable checkpoint guides do not match the READY routing result",
|
|
400
|
+
[ARTIFACT_PATHS.state, ARTIFACT_PATHS.route],
|
|
401
|
+
));
|
|
402
|
+
}
|
|
403
|
+
if (!sameStringSet(state.requiredGates ?? [], persisted.requiredGates)
|
|
404
|
+
|| !sameStringSet(state.satisfiedGates ?? [], persisted.satisfiedGates)) {
|
|
405
|
+
errors.push(issue(
|
|
406
|
+
"E_PREFLIGHT_GATES_STALE",
|
|
407
|
+
"The resumable checkpoint gate sets do not match the READY preflight",
|
|
408
|
+
[ARTIFACT_PATHS.state, ARTIFACT_PATHS.preflight, ARTIFACT_PATHS.gates],
|
|
409
|
+
));
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const ledger = await validateEventLedger(target, packageRoot);
|
|
414
|
+
if (!ledger.valid) {
|
|
415
|
+
errors.push(...ledger.errors.map((error) => issue(
|
|
416
|
+
error.code ?? "E_EVENT_INVALID",
|
|
417
|
+
error.message,
|
|
418
|
+
[ARTIFACT_PATHS.events],
|
|
419
|
+
)));
|
|
420
|
+
}
|
|
421
|
+
const events = ledger.events ?? [];
|
|
422
|
+
for (const requiredEvent of ["CONTRACT_VALIDATED", "ROUTE_VALIDATED"]) {
|
|
423
|
+
if (!events.some((event) => event.event === requiredEvent && event.taskId === persisted.taskId)) {
|
|
424
|
+
errors.push(issue(
|
|
425
|
+
"E_PREFLIGHT_EVENT_MISSING",
|
|
426
|
+
`READY preflight is missing lifecycle event: ${requiredEvent}`,
|
|
427
|
+
[ARTIFACT_PATHS.events, ARTIFACT_PATHS.preflight],
|
|
428
|
+
));
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
for (const gate of persisted.satisfiedGates ?? []) {
|
|
432
|
+
if (!events.some((event) => event.event === "GATE_SATISFIED"
|
|
433
|
+
&& event.taskId === persisted.taskId
|
|
434
|
+
&& event.details?.gate === gate)) {
|
|
435
|
+
errors.push(issue(
|
|
436
|
+
"E_PREFLIGHT_GATE_EVENT_MISSING",
|
|
437
|
+
`READY preflight is missing lifecycle gate event: ${gate}`,
|
|
438
|
+
[ARTIFACT_PATHS.events, `${ARTIFACT_PATHS.gates}/${gate}.json`],
|
|
439
|
+
));
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
const readyEvents = events.filter((event) => event.event === "PREFLIGHT_READY" && event.taskId === persisted.taskId);
|
|
443
|
+
if (readyEvents.length === 0) {
|
|
444
|
+
errors.push(issue(
|
|
445
|
+
"E_PREFLIGHT_READY_EVENT_MISSING",
|
|
446
|
+
"Persisted READY preflight is missing the matching PREFLIGHT_READY lifecycle event",
|
|
447
|
+
[ARTIFACT_PATHS.preflight, ARTIFACT_PATHS.events],
|
|
448
|
+
));
|
|
449
|
+
} else if (!readyEvents.some((event) => sameReadyPreflightEvent(event, result))) {
|
|
450
|
+
errors.push(issue(
|
|
451
|
+
"E_PREFLIGHT_READY_EVENT_MISMATCH",
|
|
452
|
+
"PREFLIGHT_READY lifecycle details do not match the persisted READY preflight",
|
|
453
|
+
[ARTIFACT_PATHS.preflight, ARTIFACT_PATHS.events],
|
|
454
|
+
));
|
|
455
|
+
}
|
|
456
|
+
return sortIssues(errors);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function sameBlockedPreflightEvent(event, result) {
|
|
460
|
+
return event.event === "PREFLIGHT_BLOCKED"
|
|
461
|
+
&& event.taskId === result.taskId
|
|
462
|
+
&& event.fingerprint === (result.fingerprints.contract ?? undefined)
|
|
463
|
+
&& sameStringSet(event.details?.requiredGates, result.requiredGates)
|
|
464
|
+
&& sameStringSet(event.details?.satisfiedGates, result.satisfiedGates)
|
|
465
|
+
&& event.details?.routingFingerprint === result.fingerprints.routing;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function assertExistingReadyLifecycleCompatibility(ledger, result) {
|
|
469
|
+
if (result.status !== "READY") return;
|
|
470
|
+
const existingReady = ledger?.events?.find((event) => event.event === "PREFLIGHT_READY" && event.taskId === result.taskId);
|
|
471
|
+
if (existingReady && !sameReadyPreflightEvent(existingReady, result)) {
|
|
356
472
|
throw preflightError(
|
|
357
473
|
"E_PHASE_CHRONOLOGY_INVALID",
|
|
358
474
|
"PREFLIGHT_READY already exists with different READY preflight details; repair the contract, route, or gate lifecycle before refreshing preflight",
|
|
359
475
|
[ARTIFACT_PATHS.preflight, ARTIFACT_PATHS.events, ARTIFACT_PATHS.contract, ARTIFACT_PATHS.route, ARTIFACT_PATHS.gates],
|
|
360
476
|
);
|
|
361
477
|
}
|
|
362
|
-
const lastMilestone = ledger.events.reduce(
|
|
363
|
-
(last, event) => Math.max(last, LIFECYCLE_MILESTONES.indexOf(event.event)),
|
|
364
|
-
-1,
|
|
365
|
-
);
|
|
366
|
-
const routeMilestone = LIFECYCLE_MILESTONES.indexOf("ROUTE_VALIDATED");
|
|
367
|
-
const preflightMilestone = LIFECYCLE_MILESTONES.indexOf("PREFLIGHT_READY");
|
|
368
|
-
if (lastMilestone < routeMilestone) return { appendEvents: false };
|
|
369
|
-
if (lastMilestone !== preflightMilestone - 1) {
|
|
370
|
-
throw preflightError(
|
|
371
|
-
"E_PHASE_CHRONOLOGY_INVALID",
|
|
372
|
-
"PREFLIGHT_READY cannot be appended after the current lifecycle ledger",
|
|
373
|
-
[ARTIFACT_PATHS.events],
|
|
374
|
-
);
|
|
375
|
-
}
|
|
376
|
-
return { appendEvents: true };
|
|
377
478
|
}
|
|
378
479
|
|
|
379
|
-
|
|
380
|
-
const
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
480
|
+
async function appendActivationEvents(target, packageRoot, ledger, result) {
|
|
481
|
+
const events = [...(ledger?.events ?? [])];
|
|
482
|
+
const hasEvent = (eventName) => events.some((event) => event.event === eventName && event.taskId === result.taskId);
|
|
483
|
+
const append = async (input) => {
|
|
484
|
+
const event = await appendProtocolEvent(target, input, packageRoot);
|
|
485
|
+
events.push(event);
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
if (events.length === 0) {
|
|
489
|
+
await append({ taskId: result.taskId, event: "TASK_RECEIVED" });
|
|
490
|
+
}
|
|
491
|
+
if (result.contract.status === "valid" && !hasEvent("CONTRACT_VALIDATED")) {
|
|
492
|
+
await append({ taskId: result.taskId, event: "CONTRACT_VALIDATED", fingerprint: result.fingerprints.contract });
|
|
493
|
+
}
|
|
494
|
+
if (result.routing.status === "valid" && !hasEvent("ROUTE_VALIDATED")) {
|
|
495
|
+
await append({ taskId: result.taskId, event: "ROUTE_VALIDATED", fingerprint: result.fingerprints.routing });
|
|
496
|
+
}
|
|
497
|
+
for (const gate of result.satisfiedGates) {
|
|
498
|
+
if (!events.some((event) => event.event === "GATE_SATISFIED"
|
|
499
|
+
&& event.taskId === result.taskId
|
|
500
|
+
&& event.details?.gate === gate)) {
|
|
501
|
+
await append({ taskId: result.taskId, event: "GATE_SATISFIED", details: { gate } });
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const existingReady = events.find((event) => event.event === "PREFLIGHT_READY" && event.taskId === result.taskId);
|
|
506
|
+
if (result.status === "READY") {
|
|
507
|
+
if (existingReady && !sameReadyPreflightEvent(existingReady, result)) {
|
|
508
|
+
throw preflightError(
|
|
509
|
+
"E_PHASE_CHRONOLOGY_INVALID",
|
|
510
|
+
"PREFLIGHT_READY already exists with different READY preflight details; repair the contract, route, or gate lifecycle before refreshing preflight",
|
|
511
|
+
[ARTIFACT_PATHS.preflight, ARTIFACT_PATHS.events, ARTIFACT_PATHS.contract, ARTIFACT_PATHS.route, ARTIFACT_PATHS.gates],
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
if (!existingReady) {
|
|
515
|
+
await append({
|
|
397
516
|
taskId: result.taskId,
|
|
398
|
-
event:
|
|
517
|
+
event: "PREFLIGHT_READY",
|
|
399
518
|
fingerprint: result.fingerprints.contract ?? undefined,
|
|
400
519
|
details: {
|
|
401
520
|
requiredGates: result.requiredGates,
|
|
402
521
|
satisfiedGates: result.satisfiedGates,
|
|
403
522
|
routingFingerprint: result.fingerprints.routing,
|
|
404
523
|
},
|
|
405
|
-
}
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
} else if (!events.some((event) => sameBlockedPreflightEvent(event, result))) {
|
|
527
|
+
await append({
|
|
528
|
+
taskId: result.taskId,
|
|
529
|
+
event: "PREFLIGHT_BLOCKED",
|
|
530
|
+
fingerprint: result.fingerprints.contract ?? undefined,
|
|
531
|
+
details: {
|
|
532
|
+
requiredGates: result.requiredGates,
|
|
533
|
+
satisfiedGates: result.satisfiedGates,
|
|
534
|
+
routingFingerprint: result.fingerprints.routing,
|
|
535
|
+
},
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
return events;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
export async function runPreflight({ target, packageRoot, strict = false, persist = true } = {}) {
|
|
542
|
+
let result = await evaluatePreflight({ target, packageRoot, strict });
|
|
543
|
+
if (!persist) return result;
|
|
544
|
+
|
|
545
|
+
let ledger = await assertPreflightPersistenceSafety(target, packageRoot, result.taskId);
|
|
546
|
+
assertPreflightResultPersistenceSafety(result);
|
|
547
|
+
assertExistingReadyLifecycleCompatibility(ledger, result);
|
|
548
|
+
|
|
549
|
+
let contract = null;
|
|
550
|
+
let route = null;
|
|
551
|
+
if (result.contract.status === "valid" && result.routing.status === "valid") {
|
|
552
|
+
contract = await readContract(target, packageRoot);
|
|
553
|
+
route = await readPersistedRoute(target, packageRoot);
|
|
554
|
+
const state = await ensureResumableState({ target, packageRoot, contract, route });
|
|
555
|
+
if (state) {
|
|
556
|
+
await synchronizePreflightState({
|
|
557
|
+
target,
|
|
558
|
+
packageRoot,
|
|
559
|
+
state,
|
|
560
|
+
contract,
|
|
561
|
+
route,
|
|
562
|
+
requiredGates: result.requiredGates,
|
|
563
|
+
satisfiedGates: result.satisfiedGates,
|
|
564
|
+
complianceMode: result.policy?.complianceMode,
|
|
565
|
+
});
|
|
566
|
+
result = await evaluatePreflight({ target, packageRoot, strict });
|
|
567
|
+
assertPreflightResultPersistenceSafety(result);
|
|
568
|
+
assertExistingReadyLifecycleCompatibility(ledger, result);
|
|
569
|
+
ledger = await assertPreflightPersistenceSafety(target, packageRoot, result.taskId);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
if (result.taskId !== "unknown") {
|
|
574
|
+
await appendActivationEvents(target, packageRoot, ledger, result);
|
|
575
|
+
const afterEvents = await validateEventLedger(target, packageRoot);
|
|
576
|
+
if (!afterEvents.valid) {
|
|
577
|
+
const first = afterEvents.errors[0];
|
|
578
|
+
throw preflightError(first.code, first.message, [ARTIFACT_PATHS.events]);
|
|
406
579
|
}
|
|
407
580
|
}
|
|
581
|
+
await writeJsonArtifact(target, ARTIFACT_PATHS.preflight, result, "preflight", packageRoot);
|
|
408
582
|
return result;
|
|
409
583
|
}
|
package/src/core/profile.js
CHANGED
|
@@ -3,6 +3,7 @@ import { readFile } from "node:fs/promises";
|
|
|
3
3
|
import { ARTIFACT_PATHS, readJsonArtifact } from "./artifacts.js";
|
|
4
4
|
import { assertSafePath, ensureWithin, fileExists } from "./filesystem.js";
|
|
5
5
|
import { assertSourceProvenance } from "./sources.js";
|
|
6
|
+
import { LEGACY_PROFILE_PATH, PROFILE_PATH } from "./target-layout.js";
|
|
6
7
|
|
|
7
8
|
const SOURCE_ID_PATTERN = /\b(?:USER|FILE|CMD|DECISION|OBS|INFER|UNKNOWN)-[A-Z0-9_-]+\b/g;
|
|
8
9
|
const DIRECTIVE_PATTERN = /forgeloop-source:\s*([A-Z0-9_-]+)\s+kind=([a-z-]+)/gi;
|
|
@@ -12,12 +13,11 @@ function issue(code, message, artifacts = []) {
|
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
export async function validateProfileSources(target, packageRoot) {
|
|
15
|
-
const profilePath =
|
|
16
|
-
|
|
17
|
-
const absolutePath = ensureWithin(target, profilePath);
|
|
18
|
-
if (!(await fileExists(absolutePath))) {
|
|
16
|
+
const profilePath = await findProfilePath(target);
|
|
17
|
+
if (!profilePath) {
|
|
19
18
|
return { status: "missing", refs: [], errors: [] };
|
|
20
19
|
}
|
|
20
|
+
const absolutePath = ensureWithin(target, profilePath);
|
|
21
21
|
const text = await readFile(absolutePath, "utf8");
|
|
22
22
|
const refs = [...new Set(text.match(SOURCE_ID_PATTERN) ?? [])].sort();
|
|
23
23
|
const directives = [...text.matchAll(DIRECTIVE_PATTERN)].map((match) => ({ id: match[1], kind: match[2] }));
|
|
@@ -46,3 +46,11 @@ export async function validateProfileSources(target, packageRoot) {
|
|
|
46
46
|
}
|
|
47
47
|
return { status: errors.length === 0 ? "valid" : "invalid", refs, errors };
|
|
48
48
|
}
|
|
49
|
+
|
|
50
|
+
export async function findProfilePath(target) {
|
|
51
|
+
for (const relativePath of [PROFILE_PATH, LEGACY_PROFILE_PATH]) {
|
|
52
|
+
await assertSafePath(target, relativePath);
|
|
53
|
+
if (await fileExists(ensureWithin(target, relativePath))) return relativePath;
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
package/src/core/protocol.js
CHANGED
|
@@ -13,6 +13,7 @@ export const FAILURE_CLASSES = Object.freeze([
|
|
|
13
13
|
"ENVIRONMENT_FAILURE",
|
|
14
14
|
"EXTERNAL_SERVICE_FAILURE",
|
|
15
15
|
"STALE_STATE_FAILURE",
|
|
16
|
+
"OPERATOR_INTERRUPTION",
|
|
16
17
|
]);
|
|
17
18
|
|
|
18
19
|
export const FAILURE_CODES = Object.freeze([
|
|
@@ -47,6 +48,12 @@ export const FAILURE_CODES = Object.freeze([
|
|
|
47
48
|
"E_COMPLETION_REJECTED",
|
|
48
49
|
"E_PUBLICATION_CLAIM_UNVERIFIED",
|
|
49
50
|
"E_PRODUCTION_READINESS_UNVERIFIED",
|
|
51
|
+
"E_STATE_MISSING_AFTER_PREFLIGHT_READY",
|
|
52
|
+
"E_PREFLIGHT_READY_EVENT_MISSING",
|
|
53
|
+
"E_PREFLIGHT_READY_EVENT_MISMATCH",
|
|
54
|
+
"E_PREFLIGHT_EVENT_MISSING",
|
|
55
|
+
"E_PREFLIGHT_GATE_EVENT_MISSING",
|
|
56
|
+
"E_PREFLIGHT_GATES_STALE",
|
|
50
57
|
]);
|
|
51
58
|
|
|
52
59
|
export const WORK_PHASES = Object.freeze([
|
|
@@ -74,6 +81,7 @@ export const GUIDE_IDS = Object.freeze([
|
|
|
74
81
|
"performance",
|
|
75
82
|
"accessibility",
|
|
76
83
|
"games",
|
|
84
|
+
"taste",
|
|
77
85
|
]);
|
|
78
86
|
|
|
79
87
|
export const GUIDE_ORDER = GUIDE_IDS;
|
package/src/core/report.js
CHANGED
|
@@ -2,9 +2,12 @@ import { readFile } from "node:fs/promises";
|
|
|
2
2
|
|
|
3
3
|
import { evaluateAudit } from "./audit.js";
|
|
4
4
|
import { fileExists, ensureWithin } from "./filesystem.js";
|
|
5
|
+
import { findProfilePath } from "./profile.js";
|
|
5
6
|
|
|
6
7
|
async function profileStatus(target) {
|
|
7
|
-
const
|
|
8
|
+
const relativePath = await findProfilePath(target);
|
|
9
|
+
if (!relativePath) return "NOT_VERIFIED";
|
|
10
|
+
const profilePath = ensureWithin(target, relativePath);
|
|
8
11
|
if (!(await fileExists(profilePath))) return "NOT_VERIFIED";
|
|
9
12
|
const text = await readFile(profilePath, "utf8");
|
|
10
13
|
return /^profile-status:\s*verified\s*$/m.test(text) ? "PASS" : "NOT_VERIFIED";
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { currentRepositoryFingerprint } from "./repository.js";
|
|
2
|
+
import { createWorkState, readWorkState, writeWorkState } from "./work-state.js";
|
|
3
|
+
|
|
4
|
+
const DEFAULT_PENDING_STEPS = ["planning", "implementation", "verification"];
|
|
5
|
+
|
|
6
|
+
export async function ensureResumableState({ target, packageRoot, contract, route }) {
|
|
7
|
+
if (!contract || !route) return null;
|
|
8
|
+
const existing = await readWorkState(target, packageRoot);
|
|
9
|
+
if (existing) return existing;
|
|
10
|
+
|
|
11
|
+
const state = createWorkState({
|
|
12
|
+
taskId: contract.value.taskId,
|
|
13
|
+
contractFingerprint: contract.fingerprint,
|
|
14
|
+
routeFingerprint: route.fingerprint,
|
|
15
|
+
repositoryFingerprint: await currentRepositoryFingerprint(target),
|
|
16
|
+
phase: "ROUTED",
|
|
17
|
+
selectedGuides: route.value.guides,
|
|
18
|
+
completedSteps: ["contract", "route"],
|
|
19
|
+
pendingSteps: DEFAULT_PENDING_STEPS,
|
|
20
|
+
checks: [],
|
|
21
|
+
failures: [],
|
|
22
|
+
blockers: [],
|
|
23
|
+
verificationEvidence: [],
|
|
24
|
+
});
|
|
25
|
+
await writeWorkState(target, state, { packageRoot });
|
|
26
|
+
return state;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function synchronizePreflightState({
|
|
30
|
+
target,
|
|
31
|
+
packageRoot,
|
|
32
|
+
state,
|
|
33
|
+
contract,
|
|
34
|
+
route,
|
|
35
|
+
requiredGates,
|
|
36
|
+
satisfiedGates,
|
|
37
|
+
complianceMode,
|
|
38
|
+
}) {
|
|
39
|
+
const candidate = createWorkState({
|
|
40
|
+
...state,
|
|
41
|
+
taskId: contract.value.taskId,
|
|
42
|
+
contractFingerprint: contract.fingerprint,
|
|
43
|
+
routeFingerprint: route.fingerprint,
|
|
44
|
+
selectedGuides: route.value.guides,
|
|
45
|
+
requiredGates: [...requiredGates],
|
|
46
|
+
satisfiedGates: [...satisfiedGates],
|
|
47
|
+
...(complianceMode ? { complianceMode } : {}),
|
|
48
|
+
lastUpdated: state.lastUpdated,
|
|
49
|
+
});
|
|
50
|
+
const withoutTimestamp = (value) => {
|
|
51
|
+
const copy = structuredClone(value);
|
|
52
|
+
delete copy.lastUpdated;
|
|
53
|
+
return copy;
|
|
54
|
+
};
|
|
55
|
+
if (JSON.stringify(withoutTimestamp(candidate)) === JSON.stringify(withoutTimestamp(state))) {
|
|
56
|
+
return state;
|
|
57
|
+
}
|
|
58
|
+
const next = { ...candidate, lastUpdated: new Date().toISOString() };
|
|
59
|
+
await writeWorkState(target, next, { packageRoot });
|
|
60
|
+
return next;
|
|
61
|
+
}
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { assertRouteInvariants } from "./router.js";
|
|
2
2
|
import { ARTIFACT_PATHS, readJsonArtifact, writeJsonArtifact } from "./artifacts.js";
|
|
3
3
|
import { readContract } from "./contract.js";
|
|
4
|
+
import { ensureResumableState } from "./resumability.js";
|
|
4
5
|
|
|
5
6
|
export async function persistRoute(target, route, packageRoot, options = {}) {
|
|
6
7
|
assertRouteInvariants(route);
|
|
7
8
|
let { contractFingerprint, ...writeOptions } = options;
|
|
9
|
+
let contractArtifact = null;
|
|
8
10
|
if (contractFingerprint === undefined) {
|
|
9
11
|
try {
|
|
10
|
-
|
|
12
|
+
contractArtifact = await readContract(target, packageRoot);
|
|
13
|
+
contractFingerprint = contractArtifact.fingerprint;
|
|
11
14
|
} catch (error) {
|
|
12
15
|
if (error.code !== "ARTIFACT_MISSING") throw error;
|
|
13
16
|
}
|
|
@@ -16,7 +19,7 @@ export async function persistRoute(target, route, packageRoot, options = {}) {
|
|
|
16
19
|
? route
|
|
17
20
|
: { ...route, contractFingerprint };
|
|
18
21
|
assertRouteInvariants(value);
|
|
19
|
-
|
|
22
|
+
const artifact = await writeJsonArtifact(
|
|
20
23
|
target,
|
|
21
24
|
ARTIFACT_PATHS.route,
|
|
22
25
|
value,
|
|
@@ -24,6 +27,10 @@ export async function persistRoute(target, route, packageRoot, options = {}) {
|
|
|
24
27
|
packageRoot,
|
|
25
28
|
writeOptions,
|
|
26
29
|
);
|
|
30
|
+
if (contractArtifact && contractArtifact.fingerprint === artifact.value.contractFingerprint) {
|
|
31
|
+
await ensureResumableState({ target, packageRoot, contract: contractArtifact, route: artifact });
|
|
32
|
+
}
|
|
33
|
+
return artifact;
|
|
27
34
|
}
|
|
28
35
|
|
|
29
36
|
export async function readPersistedRoute(target, packageRoot) {
|
package/src/core/router.js
CHANGED
|
@@ -84,7 +84,7 @@ export const PLATFORM_SEMANTICS = Object.freeze({
|
|
|
84
84
|
});
|
|
85
85
|
|
|
86
86
|
const WORK_GUIDES = Object.freeze({
|
|
87
|
-
"complete-website": ["premium", "design", "accessibility", "clean", "test", "security", "performance"],
|
|
87
|
+
"complete-website": ["premium", "design", "taste", "accessibility", "clean", "test", "security", "performance"],
|
|
88
88
|
"api-auth": ["clean", "test", "security", "performance"],
|
|
89
89
|
api: ["clean", "test"],
|
|
90
90
|
backend: ["clean", "test"],
|
|
@@ -240,6 +240,7 @@ export function evaluateRoute(input = {}) {
|
|
|
240
240
|
else if (guide === "performance") excluded[guide] = ["NO_MEASURABLE_PERFORMANCE_RISK"];
|
|
241
241
|
else if (guide === "design" || guide === "accessibility") excluded[guide] = ["NO_UI_SURFACE"];
|
|
242
242
|
else if (guide === "premium" || guide === "games") excluded[guide] = ["NO_PRIMARY_WORK_TYPE"];
|
|
243
|
+
else if (guide === "taste") excluded[guide] = ["NO_TASTE_FRONTEND_CONTEXT"];
|
|
243
244
|
else excluded[guide] = ["NO_BEHAVIOR_OR_EXECUTABLE_CHANGE"];
|
|
244
245
|
}
|
|
245
246
|
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export const LAYOUT_VERSION = 2;
|
|
2
|
+
export const LEGACY_LAYOUT_VERSION = 1;
|
|
3
|
+
export const FORGELOOP_DIR = ".forgeloop";
|
|
4
|
+
export const FORGELOOP_KIT_DIR = ".forgeloop/kit";
|
|
5
|
+
export const PROFILE_PATH = `${FORGELOOP_KIT_DIR}/PROJECT_PROFILE.md`;
|
|
6
|
+
export const LEGACY_PROFILE_PATH = "PROJECT_PROFILE.md";
|
|
7
|
+
|
|
8
|
+
export const NATIVE_ADAPTER_PATHS = Object.freeze([
|
|
9
|
+
"AGENTS.md",
|
|
10
|
+
"CLAUDE.md",
|
|
11
|
+
".cursor/rules/project-loop.mdc",
|
|
12
|
+
".github/copilot-instructions.md",
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
const NATIVE_ADAPTER_SET = new Set(NATIVE_ADAPTER_PATHS);
|
|
16
|
+
|
|
17
|
+
export function isNativeAdapterPath(relativePath) {
|
|
18
|
+
return NATIVE_ADAPTER_SET.has(relativePath);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function targetPathForSource(sourcePath) {
|
|
22
|
+
if (sourcePath === ".forgeloop/.gitignore" || isNativeAdapterPath(sourcePath)) {
|
|
23
|
+
return sourcePath;
|
|
24
|
+
}
|
|
25
|
+
return `${FORGELOOP_KIT_DIR}/${sourcePath}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function legacyPathForSource(sourcePath) {
|
|
29
|
+
return sourcePath === ".forgeloop/forgeloop.gitignore" ? ".forgeloop/.gitignore" : sourcePath;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function isKitPath(relativePath) {
|
|
33
|
+
return relativePath === FORGELOOP_KIT_DIR || relativePath.startsWith(`${FORGELOOP_KIT_DIR}/`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function profilePathForLayout(layoutVersion = LAYOUT_VERSION) {
|
|
37
|
+
return layoutVersion >= LAYOUT_VERSION ? PROFILE_PATH : LEGACY_PROFILE_PATH;
|
|
38
|
+
}
|
package/src/core/templates.js
CHANGED
|
@@ -2,6 +2,13 @@ import { readFile } from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
|
|
5
|
+
import {
|
|
6
|
+
isNativeAdapterPath,
|
|
7
|
+
legacyPathForSource,
|
|
8
|
+
targetPathForSource,
|
|
9
|
+
} from "./target-layout.js";
|
|
10
|
+
import { nativeShim } from "./native-adapters.js";
|
|
11
|
+
|
|
5
12
|
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
6
13
|
|
|
7
14
|
const TEMPLATE_SOURCE_PATHS = Object.freeze({
|
|
@@ -35,6 +42,7 @@ export const TEMPLATE_PATHS = [
|
|
|
35
42
|
"ENG/games-code-design-web-eng.md",
|
|
36
43
|
"ENG/perf-code-eng.md",
|
|
37
44
|
"ENG/premium-sites-studio-eng.md",
|
|
45
|
+
"ENG/taste-frontend-eng.md",
|
|
38
46
|
"ENG/sec-code-eng.md",
|
|
39
47
|
"ENG/test-code-eng.md",
|
|
40
48
|
"schemas/routing-input.schema.json",
|
|
@@ -66,8 +74,12 @@ export async function readTemplateEntries(packageRoot = PACKAGE_ROOT) {
|
|
|
66
74
|
TEMPLATE_PATHS.map(async (relativePath) => {
|
|
67
75
|
const sourcePath = TEMPLATE_SOURCE_PATHS[relativePath] ?? relativePath;
|
|
68
76
|
return {
|
|
69
|
-
relativePath,
|
|
70
|
-
|
|
77
|
+
relativePath: targetPathForSource(relativePath),
|
|
78
|
+
sourcePath,
|
|
79
|
+
legacyRelativePath: legacyPathForSource(sourcePath),
|
|
80
|
+
bytes: Buffer.from(isNativeAdapterPath(relativePath)
|
|
81
|
+
? nativeShim(relativePath)
|
|
82
|
+
: await readFile(path.join(packageRoot, sourcePath))),
|
|
71
83
|
};
|
|
72
84
|
}),
|
|
73
85
|
);
|