@tea-agent/loop-agent 0.28.11 → 0.28.12
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/CHANGELOG.md +29 -1
- package/README.md +3 -1
- package/dist/application/task-lifecycle/advance.js +21 -4
- package/dist/application/task-lifecycle/plan-transitions.js +17 -5
- package/dist/commands/import-prd.js +1 -1
- package/dist/commands/init.js +2 -2
- package/dist/shared/operator/capabilities.js +21 -0
- package/dist/worker/console/operator-actions.js +11 -0
- package/dist/worker/scheduler/cli.js +110 -5
- package/dist/worker/scheduler/clock-install/darwin-launchd.js +238 -0
- package/dist/worker/scheduler/clock-install/linux-systemd-user.js +224 -0
- package/dist/worker/scheduler/clock-install/types.js +1 -0
- package/dist/worker/scheduler/clock-install/win32-schtasks.js +172 -0
- package/dist/worker/scheduler/clock-install.js +284 -0
- package/dist/worker/scheduler/clock.js +420 -0
- package/dist/worker/scheduler/doctor.js +86 -1
- package/dist/worker/scheduler/index.js +2 -0
- package/dist/worker/scheduler/morning-window.js +31 -1
- package/dist/worker/scheduler/paths.js +8 -0
- package/dist/workflows/dag/frontend-implementation-contract.js +189 -6
- package/dist/workflows/dag/init-hybrid.js +26 -9
- package/dist/workflows/dag/output-protocol.js +60 -7
- package/docs/templates/init-managed-agents.md +3 -1
- package/package.json +1 -1
- package/skills/loop-agent/SKILL.md +3 -1
- package/skills/loop-agent/references/command-reference.md +9 -4
- package/skills/loop-agent/references/harness-policy.md +3 -1
|
@@ -351,6 +351,19 @@ export function extractFrontendImplementationJson(text) {
|
|
|
351
351
|
return JSON.parse(source);
|
|
352
352
|
}
|
|
353
353
|
catch (error) {
|
|
354
|
+
// Apply only deterministic, structure-preserving repairs before the
|
|
355
|
+
// quote repair below. These are common JSONC/model formatting defects,
|
|
356
|
+
// not a general parser relaxation.
|
|
357
|
+
const withoutComments = source
|
|
358
|
+
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
359
|
+
.replace(/^\s*\/\/.*$/gm, "");
|
|
360
|
+
const withoutTrailingCommas = withoutComments.replace(/,\s*([}\]])/g, "$1");
|
|
361
|
+
try {
|
|
362
|
+
return JSON.parse(withoutTrailingCommas);
|
|
363
|
+
}
|
|
364
|
+
catch {
|
|
365
|
+
// Continue with the narrower embedded-quote repair.
|
|
366
|
+
}
|
|
354
367
|
// Models sometimes put ordinary ASCII quotes inside a JSON string
|
|
355
368
|
// (for example: `reason: "支持..."`). Repair only quotes that are
|
|
356
369
|
// clearly not structural: a closing quote is followed by JSON
|
|
@@ -503,6 +516,51 @@ function asStringArray(value) {
|
|
|
503
516
|
.map((item) => asString(item))
|
|
504
517
|
.filter((item) => item.length > 0);
|
|
505
518
|
}
|
|
519
|
+
function normalizeFrontendRoute(value) {
|
|
520
|
+
const route = asString(value).replaceAll("\\", "/").replace(/^\/+/, "");
|
|
521
|
+
if (!route || route === "." || route.split("/").includes(".."))
|
|
522
|
+
return null;
|
|
523
|
+
return `/${route}`;
|
|
524
|
+
}
|
|
525
|
+
function normalizeFrontendContractOptionalFields(value) {
|
|
526
|
+
const record = asRecord(value);
|
|
527
|
+
if (!record)
|
|
528
|
+
return value;
|
|
529
|
+
const targets = asRecord(record.targets);
|
|
530
|
+
const mockApi = asRecord(record.mockApi);
|
|
531
|
+
return {
|
|
532
|
+
...record,
|
|
533
|
+
...(targets
|
|
534
|
+
? {
|
|
535
|
+
targets: {
|
|
536
|
+
...targets,
|
|
537
|
+
routes: Array.isArray(targets.routes)
|
|
538
|
+
? targets.routes.map(normalizeFrontendRoute).filter((route) => Boolean(route))
|
|
539
|
+
: targets.routes,
|
|
540
|
+
},
|
|
541
|
+
}
|
|
542
|
+
: {}),
|
|
543
|
+
...(mockApi
|
|
544
|
+
? {
|
|
545
|
+
mockApi: {
|
|
546
|
+
...mockApi,
|
|
547
|
+
endpoints: Array.isArray(mockApi.endpoints)
|
|
548
|
+
? mockApi.endpoints.map((item) => {
|
|
549
|
+
const endpoint = asRecord(item);
|
|
550
|
+
if (!endpoint)
|
|
551
|
+
return item;
|
|
552
|
+
return {
|
|
553
|
+
...endpoint,
|
|
554
|
+
fixture: asString(endpoint.fixture) || undefined,
|
|
555
|
+
consumer: asString(endpoint.consumer) || undefined,
|
|
556
|
+
};
|
|
557
|
+
})
|
|
558
|
+
: mockApi.endpoints,
|
|
559
|
+
},
|
|
560
|
+
}
|
|
561
|
+
: {}),
|
|
562
|
+
};
|
|
563
|
+
}
|
|
506
564
|
function isRequirementId(value) {
|
|
507
565
|
return REQUIREMENT_ID_PATTERN.test(value);
|
|
508
566
|
}
|
|
@@ -570,6 +628,112 @@ function canonicalizeVerificationTargetAliases(value) {
|
|
|
570
628
|
: record.requirements;
|
|
571
629
|
return { ...record, requirements };
|
|
572
630
|
}
|
|
631
|
+
/**
|
|
632
|
+
* Model-written evidence gaps are hypotheses, not authoritative coverage
|
|
633
|
+
* facts. If the plan contains a real verification target that explicitly
|
|
634
|
+
* names a requirement, the executor can prove that the requirement has a
|
|
635
|
+
* verification path even when the model forgot to wire the target into the
|
|
636
|
+
* requirement entry or conservatively marked its gap as blocking.
|
|
637
|
+
*
|
|
638
|
+
* Keep gaps blocking when that proof cannot be derived. This is deliberately
|
|
639
|
+
* narrow: it uses only sourceBinding requirement IDs and structurally present
|
|
640
|
+
* verification targets, and never invents a command, file, or target.
|
|
641
|
+
*/
|
|
642
|
+
function deriveFrontendVerificationCoverage(value, canonicalBinding) {
|
|
643
|
+
const record = asRecord(value);
|
|
644
|
+
if (!record)
|
|
645
|
+
return value;
|
|
646
|
+
const rawTargets = Array.isArray(record.verificationTargets)
|
|
647
|
+
? record.verificationTargets
|
|
648
|
+
: [];
|
|
649
|
+
const targetIds = new Set(rawTargets
|
|
650
|
+
.map((item) => asRecord(item))
|
|
651
|
+
.filter((item) => Boolean(item))
|
|
652
|
+
.map((item) => asString(item.id))
|
|
653
|
+
.filter(Boolean));
|
|
654
|
+
const targetIdsByRequirement = new Map();
|
|
655
|
+
for (const item of rawTargets) {
|
|
656
|
+
const target = asRecord(item);
|
|
657
|
+
if (!target)
|
|
658
|
+
continue;
|
|
659
|
+
const targetId = asString(target.id);
|
|
660
|
+
const commandLabel = asString(target.commandLabel) || asString(target.command);
|
|
661
|
+
const file = asString(target.file);
|
|
662
|
+
// A target is usable evidence only when it has an identity, command and
|
|
663
|
+
// file. The strict schema will validate the final shape afterwards.
|
|
664
|
+
if (!targetId || !commandLabel || !file)
|
|
665
|
+
continue;
|
|
666
|
+
const targetRequirementIds = asStringArray(target.requirementIds);
|
|
667
|
+
const inferredRequirementIds = targetRequirementIds.length > 0
|
|
668
|
+
? targetRequirementIds
|
|
669
|
+
: rawTargets.length === 1
|
|
670
|
+
? canonicalBinding.requirementIds
|
|
671
|
+
: [];
|
|
672
|
+
for (const requirementId of inferredRequirementIds) {
|
|
673
|
+
const canonicalId = canonicalizeRequirementId(requirementId);
|
|
674
|
+
if (!canonicalBinding.requirementIds.includes(canonicalId))
|
|
675
|
+
continue;
|
|
676
|
+
const ids = targetIdsByRequirement.get(canonicalId) ?? [];
|
|
677
|
+
if (!ids.includes(targetId))
|
|
678
|
+
ids.push(targetId);
|
|
679
|
+
targetIdsByRequirement.set(canonicalId, ids);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
const provenRequirementIds = new Set(targetIdsByRequirement.keys());
|
|
683
|
+
const requirements = Array.isArray(record.requirements)
|
|
684
|
+
? record.requirements.map((item) => {
|
|
685
|
+
const requirement = asRecord(item);
|
|
686
|
+
if (!requirement)
|
|
687
|
+
return item;
|
|
688
|
+
const requirementId = canonicalizeRequirementId(asString(requirement.id));
|
|
689
|
+
const inferredTargetIds = targetIdsByRequirement.get(requirementId) ?? [];
|
|
690
|
+
const existingTargetIds = asStringArray(requirement.verificationTargetIds)
|
|
691
|
+
.filter((id) => targetIds.has(id));
|
|
692
|
+
const verificationTargetIds = [...new Set([
|
|
693
|
+
...existingTargetIds,
|
|
694
|
+
...inferredTargetIds,
|
|
695
|
+
])];
|
|
696
|
+
const evidenceGap = asRecord(requirement.evidenceGap);
|
|
697
|
+
const hasProof = provenRequirementIds.has(requirementId);
|
|
698
|
+
return {
|
|
699
|
+
...requirement,
|
|
700
|
+
...(verificationTargetIds.length > 0 ? { verificationTargetIds } : {}),
|
|
701
|
+
...(hasProof
|
|
702
|
+
? (evidenceGap ? { evidenceGap: { ...evidenceGap, blocking: false } } : {})
|
|
703
|
+
: {
|
|
704
|
+
evidenceGap: {
|
|
705
|
+
requirementId,
|
|
706
|
+
description: `No executable verification target can be derived for ${requirementId}`,
|
|
707
|
+
blocking: true,
|
|
708
|
+
},
|
|
709
|
+
}),
|
|
710
|
+
};
|
|
711
|
+
})
|
|
712
|
+
: record.requirements;
|
|
713
|
+
const modelEvidenceGaps = Array.isArray(record.evidenceGaps)
|
|
714
|
+
? record.evidenceGaps.map((item) => {
|
|
715
|
+
const gap = asRecord(item);
|
|
716
|
+
if (!gap)
|
|
717
|
+
return item;
|
|
718
|
+
const requirementId = canonicalizeRequirementId(asString(gap.requirementId));
|
|
719
|
+
// Model gaps are advisory. Blocking status is reconstructed below
|
|
720
|
+
// from the source binding and executable verification targets.
|
|
721
|
+
return { ...gap, blocking: false };
|
|
722
|
+
})
|
|
723
|
+
: [];
|
|
724
|
+
const derivedBlockingGaps = canonicalBinding.requirementIds
|
|
725
|
+
.filter((requirementId) => !provenRequirementIds.has(requirementId))
|
|
726
|
+
.map((requirementId) => ({
|
|
727
|
+
requirementId,
|
|
728
|
+
description: `No executable verification target can be derived for ${requirementId}`,
|
|
729
|
+
blocking: true,
|
|
730
|
+
}));
|
|
731
|
+
return {
|
|
732
|
+
...record,
|
|
733
|
+
requirements,
|
|
734
|
+
evidenceGaps: [...modelEvidenceGaps, ...derivedBlockingGaps],
|
|
735
|
+
};
|
|
736
|
+
}
|
|
573
737
|
function assertFrontendContractPathsSafe(value) {
|
|
574
738
|
const record = asRecord(value);
|
|
575
739
|
if (!record)
|
|
@@ -700,12 +864,13 @@ export function coerceFrontendImplementationContractInput(value, canonicalBindin
|
|
|
700
864
|
: rawRecord.uiStates,
|
|
701
865
|
}
|
|
702
866
|
: value;
|
|
867
|
+
const normalizedOptionalFields = normalizeFrontendContractOptionalFields(normalizedValue);
|
|
703
868
|
// A payload can have the strict top-level shape while still containing
|
|
704
869
|
// empty requirement coverage arrays. Do not trust shape alone: route such
|
|
705
870
|
// payloads through the compatibility normalizer so targets and verification
|
|
706
871
|
// references are deterministically filled from the contract context.
|
|
707
|
-
if (looksLikeStrictFrontendContract(
|
|
708
|
-
const strictRecord = asRecord(
|
|
872
|
+
if (looksLikeStrictFrontendContract(normalizedOptionalFields)) {
|
|
873
|
+
const strictRecord = asRecord(normalizedOptionalFields);
|
|
709
874
|
const strictMockApi = asRecord(strictRecord?.mockApi);
|
|
710
875
|
if (strictMockApi &&
|
|
711
876
|
typeof strictMockApi.strategy === "string" &&
|
|
@@ -730,10 +895,28 @@ export function coerceFrontendImplementationContractInput(value, canonicalBindin
|
|
|
730
895
|
: verificationIds,
|
|
731
896
|
};
|
|
732
897
|
});
|
|
733
|
-
|
|
898
|
+
const strictMockApi = asRecord(strictRecord.mockApi);
|
|
899
|
+
const strictTargetFiles = asStringArray(asRecord(strictRecord.targets)?.files);
|
|
900
|
+
const mockEndpoints = Array.isArray(strictMockApi?.endpoints)
|
|
901
|
+
? strictMockApi.endpoints.map((item) => {
|
|
902
|
+
const endpoint = asRecord(item);
|
|
903
|
+
if (!endpoint)
|
|
904
|
+
return item;
|
|
905
|
+
return {
|
|
906
|
+
...endpoint,
|
|
907
|
+
consumer: asString(endpoint.consumer) ||
|
|
908
|
+
strictTargetFiles[0],
|
|
909
|
+
};
|
|
910
|
+
})
|
|
911
|
+
: strictMockApi?.endpoints;
|
|
912
|
+
return {
|
|
913
|
+
...strictRecord,
|
|
914
|
+
requirements,
|
|
915
|
+
...(strictMockApi ? { mockApi: { ...strictMockApi, endpoints: mockEndpoints } } : {}),
|
|
916
|
+
};
|
|
734
917
|
}
|
|
735
918
|
}
|
|
736
|
-
const record = asRecord(
|
|
919
|
+
const record = asRecord(normalizedOptionalFields);
|
|
737
920
|
if (!record)
|
|
738
921
|
return value;
|
|
739
922
|
const implementation = asRecord(record.implementation);
|
|
@@ -1069,10 +1252,10 @@ export async function materializeFrontendImplementationContract(input) {
|
|
|
1069
1252
|
const normalizedContract = coerceFrontendImplementationContractInput(parsed, canonicalBinding);
|
|
1070
1253
|
// There is exactly one post-security candidate. A fallback candidate would
|
|
1071
1254
|
// allow malformed raw fields to bypass the boundary checks above.
|
|
1072
|
-
const candidate = {
|
|
1255
|
+
const candidate = deriveFrontendVerificationCoverage({
|
|
1073
1256
|
...(asRecord(normalizedContract) ?? parsed),
|
|
1074
1257
|
sourceBinding: canonicalBinding,
|
|
1075
|
-
};
|
|
1258
|
+
}, canonicalBinding);
|
|
1076
1259
|
const result = frontendImplementationContractSchema.safeParse(candidate);
|
|
1077
1260
|
if (!result.success)
|
|
1078
1261
|
throw new Error(`invalid-output: ${result.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ")}`);
|
|
@@ -1300,9 +1300,19 @@ async function discoverFrontendFallbackVerifyCommands(repoRoot) {
|
|
|
1300
1300
|
"test:e2e",
|
|
1301
1301
|
"e2e",
|
|
1302
1302
|
]);
|
|
1303
|
+
if (staticCommands.length === 0 && behaviorCommands.length === 0) {
|
|
1304
|
+
try {
|
|
1305
|
+
await access(path.join(repoRoot, "node_modules", ".bin", "tsc"));
|
|
1306
|
+
staticCommands.push("npx --no-install tsc --noEmit");
|
|
1307
|
+
}
|
|
1308
|
+
catch {
|
|
1309
|
+
// No local TypeScript binary: leave verification unavailable rather
|
|
1310
|
+
// than invoking a missing script or downloading a package.
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1303
1313
|
return {
|
|
1304
|
-
staticCommands: staticCommands.length > 0 ? staticCommands :
|
|
1305
|
-
behaviorCommands: behaviorCommands.length > 0 ? behaviorCommands :
|
|
1314
|
+
staticCommands: staticCommands.length > 0 ? staticCommands : [],
|
|
1315
|
+
behaviorCommands: behaviorCommands.length > 0 ? behaviorCommands : [],
|
|
1306
1316
|
};
|
|
1307
1317
|
}
|
|
1308
1318
|
function deriveFrontendBehaviorPaths(taskConfig) {
|
|
@@ -5956,12 +5966,12 @@ async function buildSoftVerifyNode(sources) {
|
|
|
5956
5966
|
const strategy = resolveDagVerifyStrategy(sources.taskConfig, "1");
|
|
5957
5967
|
const fallbackCommands = sources.repoRoot
|
|
5958
5968
|
? (await discoverFrontendFallbackVerifyCommands(sources.repoRoot)).staticCommands
|
|
5959
|
-
: [
|
|
5969
|
+
: [];
|
|
5960
5970
|
const candidatePlan = canonicalizeVerificationCommands([
|
|
5961
5971
|
...taskVerifyCommands(sources.repoRoot, sources.taskConfig),
|
|
5962
5972
|
...(sources.verifyCommands?.intermediate ?? []),
|
|
5963
5973
|
]);
|
|
5964
|
-
const focusedCandidates = candidatePlan.commands.filter((command) => !isFullSuiteVerifyCommand(command));
|
|
5974
|
+
const focusedCandidates = candidatePlan.commands.filter((command) => !isFullSuiteVerifyCommand(command) && !isFrontendLintVerifyCommand(command));
|
|
5965
5975
|
const quota = verifyQuotaLimit(strategy.intermediateQuota ?? "full");
|
|
5966
5976
|
const focusedIntermediate = quota
|
|
5967
5977
|
? focusedCandidates.slice(0, quota)
|
|
@@ -5971,11 +5981,14 @@ async function buildSoftVerifyNode(sources) {
|
|
|
5971
5981
|
commands: focusedIntermediate,
|
|
5972
5982
|
taskConfig: sources.taskConfig,
|
|
5973
5983
|
});
|
|
5974
|
-
const
|
|
5984
|
+
const commandsWithoutFallback = buildVerifyShellCommands({
|
|
5975
5985
|
repoRoot: sources.repoRoot,
|
|
5976
5986
|
commands: plannedIntermediate.commands,
|
|
5977
5987
|
fallbackCommands,
|
|
5978
5988
|
});
|
|
5989
|
+
const commands = commandsWithoutFallback.length > 0
|
|
5990
|
+
? commandsWithoutFallback
|
|
5991
|
+
: ["node -e \"console.log('No project verification command configured; verification skipped')\""];
|
|
5979
5992
|
return {
|
|
5980
5993
|
id: "soft-verify-shell",
|
|
5981
5994
|
depends_on: [implementId],
|
|
@@ -5994,7 +6007,7 @@ async function buildSoftVerifyNode(sources) {
|
|
|
5994
6007
|
quota: strategy.intermediateQuota ?? "full",
|
|
5995
6008
|
commandSource: sources.verifyCommands ? "adapter" : "inline",
|
|
5996
6009
|
commands: plannedIntermediate.commands,
|
|
5997
|
-
fallbackCommands,
|
|
6010
|
+
fallbackCommands: commands,
|
|
5998
6011
|
commandTexts: commands,
|
|
5999
6012
|
commandTimeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
|
|
6000
6013
|
plan: candidatePlan,
|
|
@@ -6094,9 +6107,13 @@ function buildRepairNode(sources) {
|
|
|
6094
6107
|
.join("\n\n"),
|
|
6095
6108
|
};
|
|
6096
6109
|
}
|
|
6097
|
-
function buildHardVerifyNode(sources) {
|
|
6110
|
+
async function buildHardVerifyNode(sources) {
|
|
6098
6111
|
const repairId = repairNodeId();
|
|
6112
|
+
const discoveredFallback = sources.repoRoot
|
|
6113
|
+
? await discoverFrontendFallbackVerifyCommands(sources.repoRoot)
|
|
6114
|
+
: { staticCommands: [], behaviorCommands: [] };
|
|
6099
6115
|
const fallbackCommands = [
|
|
6116
|
+
...discoveredFallback.staticCommands.filter((command) => /(?:^|\s)(?:npm|pnpm|yarn|bun)\s+run\s+build(?:\s|$)/.test(command)),
|
|
6100
6117
|
"HARNESS_ALLOW_ACTIVE_DAG_RUNS=1 bash scripts/check-repo.sh",
|
|
6101
6118
|
];
|
|
6102
6119
|
const finalPlan = canonicalizeVerificationCommands([
|
|
@@ -6105,7 +6122,7 @@ function buildHardVerifyNode(sources) {
|
|
|
6105
6122
|
]);
|
|
6106
6123
|
const plannedFinal = applyMavenVerificationPlanning({
|
|
6107
6124
|
repoRoot: sources.repoRoot,
|
|
6108
|
-
commands: finalPlan.commands,
|
|
6125
|
+
commands: finalPlan.commands.filter((command) => !isFrontendLintVerifyCommand(command)),
|
|
6109
6126
|
taskConfig: sources.taskConfig,
|
|
6110
6127
|
});
|
|
6111
6128
|
const commands = buildVerifyShellCommands({
|
|
@@ -6274,7 +6291,7 @@ async function buildSupervisedHybridDag(standard, sources) {
|
|
|
6274
6291
|
buildProcessSupervisorNode(sources),
|
|
6275
6292
|
buildProcessGateNode(sources),
|
|
6276
6293
|
buildRepairNode(sources),
|
|
6277
|
-
buildHardVerifyNode(sources),
|
|
6294
|
+
await buildHardVerifyNode(sources),
|
|
6278
6295
|
...(authorityDecision.enabled
|
|
6279
6296
|
? [
|
|
6280
6297
|
buildAuthoritySurfaceAuditNode(sources),
|
|
@@ -62,23 +62,76 @@ export function firstNonEmptyLine(text) {
|
|
|
62
62
|
}
|
|
63
63
|
return undefined;
|
|
64
64
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
65
|
+
function extractUniqueReviewJson(text) {
|
|
66
|
+
const source = String(text).trim();
|
|
67
|
+
const blocks = [...source.matchAll(/```(?:json|jsonc)?\s*([\s\S]*?)\s*```/gi)].map((match) => match[1].trim());
|
|
68
|
+
const candidates = blocks.length > 0 ? blocks : [source];
|
|
69
|
+
const parsed = [];
|
|
70
|
+
for (const candidate of candidates) {
|
|
71
|
+
let cursor = 0;
|
|
72
|
+
while (cursor < candidate.length) {
|
|
73
|
+
const start = candidate.indexOf("{", cursor);
|
|
74
|
+
if (start < 0)
|
|
75
|
+
break;
|
|
76
|
+
let depth = 0;
|
|
77
|
+
let inString = false;
|
|
78
|
+
let escaped = false;
|
|
79
|
+
let end = -1;
|
|
80
|
+
for (let index = start; index < candidate.length; index += 1) {
|
|
81
|
+
const character = candidate[index];
|
|
82
|
+
if (inString) {
|
|
83
|
+
if (escaped)
|
|
84
|
+
escaped = false;
|
|
85
|
+
else if (character === "\\")
|
|
86
|
+
escaped = true;
|
|
87
|
+
else if (character === '"')
|
|
88
|
+
inString = false;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (character === '"')
|
|
92
|
+
inString = true;
|
|
93
|
+
else if (character === "{")
|
|
94
|
+
depth += 1;
|
|
95
|
+
else if (character === "}" && --depth === 0) {
|
|
96
|
+
try {
|
|
97
|
+
const value = JSON.parse(candidate.slice(start, index + 1));
|
|
98
|
+
if (value && typeof value === "object" && !Array.isArray(value))
|
|
99
|
+
parsed.push(value);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
// Keep fail-closed behavior for malformed JSON.
|
|
103
|
+
}
|
|
104
|
+
end = index;
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (end < 0)
|
|
109
|
+
break;
|
|
110
|
+
cursor = end + 1;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (parsed.length !== 1) {
|
|
114
|
+
throw new Error(parsed.length === 0
|
|
115
|
+
? "output must contain one valid JSON review object"
|
|
116
|
+
: "output must contain exactly one JSON review object");
|
|
117
|
+
}
|
|
118
|
+
return parsed[0];
|
|
119
|
+
}
|
|
120
|
+
/** Parse a JSON review verdict after extracting one unambiguous JSON object.
|
|
121
|
+
* Prose and one Markdown fence are tolerated at this boundary; ambiguity and
|
|
122
|
+
* schema/semantic violations remain fail-closed. */
|
|
70
123
|
export function parseJsonReviewVerdict(text) {
|
|
71
124
|
const trimmed = String(text).trim();
|
|
72
125
|
if (!trimmed)
|
|
73
126
|
return { ok: false, reason: "missing JSON output" };
|
|
74
127
|
let parsed;
|
|
75
128
|
try {
|
|
76
|
-
parsed =
|
|
129
|
+
parsed = extractUniqueReviewJson(trimmed);
|
|
77
130
|
}
|
|
78
131
|
catch (error) {
|
|
79
132
|
return {
|
|
80
133
|
ok: false,
|
|
81
|
-
reason: `output
|
|
134
|
+
reason: `output JSON extraction failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
82
135
|
};
|
|
83
136
|
}
|
|
84
137
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
@@ -99,7 +99,7 @@ loop-agent task advance <task-id> "任务标题" \
|
|
|
99
99
|
--prd <path-to-prd.md> \
|
|
100
100
|
--allowed-path "<glob>" \
|
|
101
101
|
--forbidden-path ".harness/**" \
|
|
102
|
-
--verify "
|
|
102
|
+
--verify "<label>:<command>" \
|
|
103
103
|
--json
|
|
104
104
|
# 审查 writeSet gate digest 后:
|
|
105
105
|
loop-agent task advance <task-id> --approve-gate "write-set-review:<digest>" --json
|
|
@@ -108,6 +108,8 @@ loop-agent task status <task-id> --json
|
|
|
108
108
|
# 有 plan 时收尾:loop-agent plan complete <plan-id> --summary "..."
|
|
109
109
|
```
|
|
110
110
|
|
|
111
|
+
`--verify` 命令应取项目 `AGENTS.md` / `__LOOP_AGENT_GOVERNANCE_ROOT__/verification-matrix.md` 登记的验证命令(不要假定 `npm run typecheck` 存在);`--verify` 可选,省略时自动从 package.json scripts 或既有 managed `task.json.verifyCommands` 推导建议。
|
|
112
|
+
|
|
111
113
|
`source/需求.md` 与 `source/执行约束.md` 仍必需(M8/M9),但默认由 `task advance --prd` 投影生成,而不是主会话手写。`plan create` 不是 `task advance` 的硬依赖。写入前同步 `task.json.allowedPaths` / `task.json.forbiddenPaths` 并审查 writer `writeSet`。高级任意 DagSpec 才用 `dag validate|execute|report`,不进入标准 happy path。
|
|
112
114
|
|
|
113
115
|
凡是影响项目公共契约、执行入口、交付流水线、自动化/治理、数据模型、安全或权限模型、跨模块行为、用户可见工作流的改动,都必须在编辑实现文件前先通过 `task advance` 建立 managed contract / writeSet gate,并审查 gate digest。
|
package/package.json
CHANGED
|
@@ -32,12 +32,14 @@ references:
|
|
|
32
32
|
```bash
|
|
33
33
|
loop-agent task advance <task-id> "Title" --prd <prd.md> \
|
|
34
34
|
--allowed-path "<glob>" --forbidden-path ".harness/**" \
|
|
35
|
-
--verify "
|
|
35
|
+
--verify "<label>:<command>" --json
|
|
36
36
|
loop-agent task advance <task-id> \
|
|
37
37
|
--approve-gate "write-set-review:<digest>" --json
|
|
38
38
|
loop-agent task status <task-id> --json
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
+
`--verify` 命令应取项目 `AGENTS.md` / 治理文档 verification-matrix.md 登记的验证命令(不要假定 `npm run typecheck` 存在);`--verify` 可选,省略时自动从 package.json scripts 或既有 managed `task.json.verifyCommands` 推导建议。
|
|
42
|
+
|
|
41
43
|
非微小跨会话任务可先 `plan create`。主会话只审 gate/writeSet 与验证证据,不拼低层 prepare/run-task/run-dag/promote/closeout 命令串。
|
|
42
44
|
|
|
43
45
|
## 主题路由
|
|
@@ -42,7 +42,7 @@ loop-agent doctor
|
|
|
42
42
|
--accept-recommendations DIGEST \
|
|
43
43
|
--allowed-path "src/**" \
|
|
44
44
|
--forbidden-path ".harness/**" \
|
|
45
|
-
--verify "
|
|
45
|
+
--verify "<label>:<command>" \
|
|
46
46
|
--json
|
|
47
47
|
|
|
48
48
|
# 审查 gate.writeSet / gate.digest 后
|
|
@@ -55,6 +55,8 @@ loop-agent doctor
|
|
|
55
55
|
loop-agent plan complete PLAN_ID --summary "..."
|
|
56
56
|
```
|
|
57
57
|
|
|
58
|
+
`--verify` 命令应取项目 `AGENTS.md` / `docs/governance/verification-matrix.md` 登记的验证命令(不要假定 `npm run typecheck` 存在);`--verify` 可选,省略时自动从 package.json scripts 或既有 managed `task.json.verifyCommands` 推导建议。
|
|
59
|
+
|
|
58
60
|
首次 `task advance` 内部完成 create、PRD 归档、managed contract 投影、DAG 生成与 strict validate,并停在 writeSet gate;批准后同一命令长跑到稳定终态并做确定性 promotion/closeout。决策表与反模式见 `source-and-plan-practice.md`。
|
|
59
61
|
2. **Operator 工具**,用于 recovery、诊断、评测重放:
|
|
60
62
|
|
|
@@ -166,7 +168,7 @@ loop-agent examples copy <name> --output examples/<name>
|
|
|
166
168
|
loop-agent task advance <task-id> "Task Title" \
|
|
167
169
|
--prd path/to-prd.md \
|
|
168
170
|
--allowed-path "src/**" \
|
|
169
|
-
--verify "
|
|
171
|
+
--verify "<label>:<command>" \
|
|
170
172
|
--json
|
|
171
173
|
loop-agent task advance <task-id> --approve-gate "write-set-review:<digest>" --json
|
|
172
174
|
loop-agent task status <task-id> --json
|
|
@@ -218,7 +220,7 @@ loop-agent task advance <task-id> "标题" \
|
|
|
218
220
|
--prd <path-to-original-prd.md> \
|
|
219
221
|
--allowed-path "<glob>" \
|
|
220
222
|
--forbidden-path ".harness/**" \
|
|
221
|
-
--verify "
|
|
223
|
+
--verify "<label>:<command>" \
|
|
222
224
|
--json
|
|
223
225
|
```
|
|
224
226
|
|
|
@@ -566,6 +568,9 @@ agent-worker scheduler cancel <schedule-id> --repo <repo-root> [--reason <text>]
|
|
|
566
568
|
agent-worker scheduler harvest <schedule-id> --repo <repo-root> [--json]
|
|
567
569
|
agent-worker scheduler discard <schedule-id> --repo <repo-root> --reason <text> [--force] [--json]
|
|
568
570
|
agent-worker scheduler tick --repo <repo-root> [--json] # daemon/clock; not Operator Chat
|
|
571
|
+
agent-worker scheduler clock install --repo <repo-root> [--interval-sec 60] [--loop-agent-bin <path>] [--json] # OS timer; CLI-only
|
|
572
|
+
agent-worker scheduler clock status --repo <repo-root> [--json] # CLI-only
|
|
573
|
+
agent-worker scheduler clock uninstall --repo <repo-root> [--json] # CLI-only
|
|
569
574
|
agent-worker scheduler submit --repo <repo-root> --feature-id <id> --task-id <id> ... # low-level planning fact
|
|
570
575
|
agent-worker scheduler transition <schedule-id> --repo <repo-root> --to <status> ... # internal lifecycle
|
|
571
576
|
agent-worker console [--repo <repo-root>] [--port 8790] [--host 127.0.0.1] # Official 裸入口;repo 默认当前目录
|
|
@@ -596,7 +601,7 @@ agent-worker observe snapshot --repo <repo-root> # 输出 GlobalSnapshot JSON
|
|
|
596
601
|
- `task draft-followup` 会按全部 failure category 生成 TaskDraft 或人工行动卡:ProductBug/TestBug/FlakyTest/DependencyFailure 可批准;EnvFailure 连续两次后才生成 ENV-CHECK;Spec/Contract/Risk/Human/Unknown 只给行动卡。人工以 `feature approve-followup --dry-run` 预览,再带非空 `--owner` 批准 TaskDraft;行动卡不能批准。批准在 staging validation 后写 TaskSpec、graph、Ready/approval/event,原失败事实不改写,并有 rename/state/approval/index/event 回滚门禁。
|
|
597
602
|
- `task retry` 是失败 Task 的唯一重试入口。它会保留原有运行记录和 failure handoff,并让下一次 `batch run-ready` 使用新的 `workerRunId`;不要删除运行态文件或手动修改状态来重试。
|
|
598
603
|
- **推荐**裸入口 `agent-worker console`(repo 默认当前目录,默认 `127.0.0.1:8790`)提供 Operate + Inspect;`console serve` 是等价兼容入口。Inspect 路径为 `/inspect/#/...`,API 仍为根 `/api/**`。`observe serve` 为兼容入口(默认 `8787`,启动时 stderr 输出 `OBSERVE_SERVE_DEPRECATED`);`observe snapshot` 保留。Inspect/Observe 本身不会启动、暂停或重试 Task / Worker / DAG。
|
|
599
|
-
- Night Scheduler(本地夜间自治):白天 `admission prepare` 冻结 worktree + DAG writeSet gate;`scheduler add` 消费 gate 并预约 Task Pool `Queued`(不立即执行);`scheduler
|
|
604
|
+
- Night Scheduler(本地夜间自治):白天 `admission prepare` 冻结 worktree + DAG writeSet gate;`scheduler add` 消费 gate 并预约 Task Pool `Queued`(不立即执行);`scheduler clock install` 安装本机 user-level OS timer(launchd / systemd --user / schtasks)周期调用 `scheduler tick`;tick 写统一 Clock receipt,doctor/morning/status 可解释 missing/stale/error/busy/drift;成功后 `pending-harvest`,早晨 `scheduler harvest`(exact-base FF)或 `scheduler discard`;`report morning --window night` 与 Inspect `#/night` / Operate 夜间面板读取同一套 facts。Clock install/status/uninstall 为 CLI-only。
|
|
600
605
|
- 当前 Worker 仍是 v0(库 + CLI + dogfood);日间批处理未强制定时/CI 驱动;`report morning` 默认可从 Task Pool runs 汇总,并支持 `--window night` 投影 Scheduler facts。
|
|
601
606
|
|
|
602
607
|
### 查看 duration statistics / context usage
|
|
@@ -57,7 +57,7 @@ Minimum governed path:
|
|
|
57
57
|
loop-agent task advance <task-id> "Task Title" \
|
|
58
58
|
--prd <path-to-original-prd.md> \
|
|
59
59
|
--allowed-path "<glob>" \
|
|
60
|
-
--verify "
|
|
60
|
+
--verify "<label>:<command>" \
|
|
61
61
|
--json \
|
|
62
62
|
[--repo-root <target-repo>]
|
|
63
63
|
|
|
@@ -70,6 +70,8 @@ loop-agent task advance <task-id> \
|
|
|
70
70
|
loop-agent task status <task-id> --json [--repo-root <target-repo>]
|
|
71
71
|
```
|
|
72
72
|
|
|
73
|
+
The `--verify` command should reference verification commands registered in the project's `AGENTS.md` or its governance verification-matrix.md; do not assume `npm run typecheck` exists. `--verify` is optional; when omitted, suggestions are auto-derived from `package.json` scripts or existing managed `task.json.verifyCommands`.
|
|
74
|
+
|
|
73
75
|
`loop-agent` is the preferred global CLI. For self-hosting loop-agent development, the controller must be an installed npm-published package. Use `npm install -g @tea-agent/loop-agent@latest` for first install or intentional upgrades, then treat the installed version as frozen for the current task and record `npm list -g @tea-agent/loop-agent --depth=0`. Do not repeatedly fetch `npx @latest` inside DAG nodes, and do not use the current working tree's `npm link` or `npm run dev` to control tasks that may edit CLI, DAG runtime, executors, package metadata, or build output. Use `npm run dev -- <args>` only for source debugging and focused CLI development.
|
|
74
76
|
|
|
75
77
|
The npm package carries static capability assets: `skills/` (bundled in-package, containing `loop-agent` and `agent-worker`; `loop-agent init` mirrors them into the target project's `.agents/skills/`), `docs/templates/`, `docs/architecture/`, `docs/skills/`, `examples/`, `harness.json`, `AGENTS.md`, `README.md`, and `CHANGELOG.md`. `loop-agent init` creates `ai_workspace/loop-agent/` (the default governanceRoot) in the target project with `progress/`, `reports/`, `exec-plans/`, and `decisions/` directories plus their README files; the actual files under those directories belong to the target repository and are not shipped by the npm package.
|