@deftai/directive-core 0.94.0 → 0.96.0
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/dist/authz/classify.js +485 -0
- package/dist/cache/archive.d.ts +134 -0
- package/dist/cache/archive.js +630 -0
- package/dist/cache/index.d.ts +1 -0
- package/dist/cache/index.js +1 -0
- package/dist/cache/main.js +298 -1
- package/dist/content-contracts/skills/helpers.d.ts +1 -1
- package/dist/content-contracts/skills/helpers.js +1 -0
- package/dist/doctor/main.js +41 -9
- package/dist/doctor/types.d.ts +2 -2
- package/dist/freshness/bind.d.ts +67 -0
- package/dist/freshness/bind.js +201 -0
- package/dist/freshness/cli.d.ts +30 -0
- package/dist/freshness/cli.js +180 -0
- package/dist/freshness/compare.d.ts +14 -0
- package/dist/freshness/compare.js +134 -0
- package/dist/freshness/generation.d.ts +44 -0
- package/dist/freshness/generation.js +172 -0
- package/dist/freshness/index.d.ts +13 -0
- package/dist/freshness/index.js +13 -0
- package/dist/freshness/report.d.ts +43 -0
- package/dist/freshness/report.js +139 -0
- package/dist/freshness/types.d.ts +70 -0
- package/dist/freshness/types.js +30 -0
- package/dist/handoff-evidence/index.d.ts +8 -0
- package/dist/handoff-evidence/index.js +7 -0
- package/dist/handoff-evidence/validate.d.ts +105 -0
- package/dist/handoff-evidence/validate.js +423 -0
- package/dist/hooks/dispatcher.d.ts +3 -0
- package/dist/hooks/dispatcher.js +12 -3
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/init-deposit/agent-hooks.d.ts +1 -1
- package/dist/init-deposit/agent-hooks.js +5 -2
- package/dist/init-deposit/hygiene.d.ts +1 -1
- package/dist/init-deposit/hygiene.js +16 -4
- package/dist/init-deposit/init-deposit.d.ts +7 -1
- package/dist/init-deposit/init-deposit.js +24 -4
- package/dist/init-deposit/refresh.d.ts +9 -1
- package/dist/init-deposit/refresh.js +48 -5
- package/dist/init-deposit/scaffold.js +6 -5
- package/dist/parent-turn-shape/evaluate.d.ts +84 -0
- package/dist/parent-turn-shape/evaluate.js +353 -0
- package/dist/parent-turn-shape/index.d.ts +8 -0
- package/dist/parent-turn-shape/index.js +8 -0
- package/dist/policy/index.d.ts +3 -0
- package/dist/policy/index.js +35 -0
- package/dist/release-e2e/greenfield-python-free-smoke.js +7 -3
- package/dist/review-monitor/constants.js +3 -2
- package/dist/review-monitor/tier-detection.d.ts +7 -3
- package/dist/review-monitor/tier-detection.js +18 -1
- package/dist/review-monitor/verify.js +18 -0
- package/dist/scope/index.d.ts +2 -0
- package/dist/scope/index.js +2 -0
- package/dist/scope/main.d.ts +10 -0
- package/dist/scope/main.js +109 -24
- package/dist/scope/promote-from-issue.d.ts +49 -0
- package/dist/scope/promote-from-issue.js +367 -0
- package/dist/scope/promote-path.d.ts +39 -0
- package/dist/scope/promote-path.js +105 -0
- package/dist/session/ritual-entrypoint.d.ts +6 -2
- package/dist/session/ritual-entrypoint.js +15 -2
- package/dist/session/session-ready.js +31 -14
- package/dist/session/session-start.d.ts +2 -1
- package/dist/session/session-start.js +66 -3
- package/dist/session/verify-session-ritual.d.ts +4 -2
- package/dist/session/verify-session-ritual.js +11 -11
- package/dist/slash/product-set.js +4 -1
- package/dist/swarm/routing.d.ts +4 -2
- package/dist/swarm/routing.js +26 -4
- package/dist/swarm/verify-review-clean.d.ts +4 -3
- package/dist/swarm/verify-review-clean.js +28 -73
- package/dist/triage/actions/index.js +62 -2
- package/dist/triage/actions/types.d.ts +8 -1
- package/dist/triage/author-filter.d.ts +51 -0
- package/dist/triage/author-filter.js +152 -0
- package/dist/triage/classify/index.d.ts +9 -0
- package/dist/triage/classify/index.js +34 -2
- package/dist/triage/classify/label-mirror.d.ts +206 -0
- package/dist/triage/classify/label-mirror.js +914 -0
- package/dist/triage/help/registry-data.d.ts +49 -38
- package/dist/triage/help/registry-data.js +134 -39
- package/dist/triage/index.d.ts +1 -0
- package/dist/triage/index.js +1 -0
- package/dist/triage/queue/index.d.ts +1 -0
- package/dist/triage/queue/index.js +1 -0
- package/dist/triage/queue/render.d.ts +2 -0
- package/dist/triage/queue/render.js +6 -0
- package/dist/triage/queue/show.d.ts +1 -1
- package/dist/triage/queue/show.js +4 -2
- package/dist/vbrief-validate/project-definition.js +1 -1
- package/dist/verify-env/agent-hook-readiness.d.ts +42 -0
- package/dist/verify-env/agent-hook-readiness.js +150 -0
- package/dist/verify-env/agent-hooks-live-probe.d.ts +16 -4
- package/dist/verify-env/agent-hooks-live-probe.js +120 -107
- package/dist/verify-env/agent-hooks.js +11 -3
- package/dist/verify-env/index.d.ts +1 -0
- package/dist/verify-env/index.js +1 -0
- package/package.json +11 -3
|
@@ -13,8 +13,10 @@ import { copyTree } from "../deposit/copy-tree.js";
|
|
|
13
13
|
import { prunePythonArtifactsFromDeposit } from "../deposit/python-free.js";
|
|
14
14
|
import { resolveInstalledContentRoot } from "../deposit/resolve-content.js";
|
|
15
15
|
import { readCorePackageVersion } from "../engine-version.js";
|
|
16
|
+
import { stampLiveGeneration } from "../freshness/generation.js";
|
|
16
17
|
import { renderProjectDefinition } from "../render/project-render.js";
|
|
17
18
|
import { depositOpenClawL2ProductCommands } from "../slash/openclaw-deposit.js";
|
|
19
|
+
import { agentHookReadinessJson, evaluateAgentHookReadiness, evaluateAgentHookReadinessSafely, } from "../verify-env/agent-hook-readiness.js";
|
|
18
20
|
import { removeStaleMigratedFrameworkNarrative } from "../xbrief-migrate/migrate-project.js";
|
|
19
21
|
import { writeAgentHookDeposit } from "./agent-hooks.js";
|
|
20
22
|
import { ensureInitGitignoreLines, reconstituteDepositFromContent } from "./gitignore.js";
|
|
@@ -81,9 +83,12 @@ function readContentVersion(contentRoot, readVersion = readCorePackageVersion) {
|
|
|
81
83
|
}
|
|
82
84
|
return readVersion();
|
|
83
85
|
}
|
|
84
|
-
export function buildInstallSummaryJson(
|
|
86
|
+
export function buildInstallSummaryJson(input) {
|
|
87
|
+
const { result, options, readiness } = input;
|
|
85
88
|
return {
|
|
86
|
-
success: true,
|
|
89
|
+
success: readiness ? readiness.code === 0 : true,
|
|
90
|
+
deposit_completed: true,
|
|
91
|
+
...(readiness ? { agent_hook_readiness: agentHookReadinessJson(readiness) } : {}),
|
|
87
92
|
action: "install",
|
|
88
93
|
version: readCorePackageVersion(),
|
|
89
94
|
project_dir: result.projectDir,
|
|
@@ -210,6 +215,14 @@ export async function runInitDeposit(args, io, seams = {}) {
|
|
|
210
215
|
includeTaskfile: taskfileWired,
|
|
211
216
|
});
|
|
212
217
|
printCommitGuidance(io, stagePaths, staged);
|
|
218
|
+
// #3117: stamp live generation only after required init projections succeed.
|
|
219
|
+
// Stamping earlier would advance authority for a failed/partial init (Greptile).
|
|
220
|
+
stampLiveGeneration(projectDir, {
|
|
221
|
+
contentVersion: version,
|
|
222
|
+
stampedBy: "directive-init",
|
|
223
|
+
increment: true,
|
|
224
|
+
nowIso: nowIso(),
|
|
225
|
+
});
|
|
213
226
|
return {
|
|
214
227
|
projectDir,
|
|
215
228
|
deftDir,
|
|
@@ -234,14 +247,21 @@ export async function runInitDepositCli(options) {
|
|
|
234
247
|
};
|
|
235
248
|
try {
|
|
236
249
|
const result = await runInitDeposit(options, io, options.seams);
|
|
250
|
+
const readiness = evaluateAgentHookReadinessSafely(result.projectDir, options.seams?.evaluateAgentHookReadiness ?? evaluateAgentHookReadiness);
|
|
237
251
|
if (options.jsonOut) {
|
|
238
|
-
options.writeOut(`${JSON.stringify(buildInstallSummaryJson(result, options), null, 2)}\n`);
|
|
252
|
+
options.writeOut(`${JSON.stringify(buildInstallSummaryJson({ result, options, readiness }), null, 2)}\n`);
|
|
239
253
|
printNextSteps(result, { printf: options.writeErr });
|
|
240
254
|
}
|
|
241
255
|
else {
|
|
242
256
|
printNextSteps(result, io);
|
|
243
257
|
}
|
|
244
|
-
|
|
258
|
+
const readinessOut = options.jsonOut
|
|
259
|
+
? options.writeErr
|
|
260
|
+
: readiness.stream === "stderr"
|
|
261
|
+
? options.writeErr
|
|
262
|
+
: options.writeOut;
|
|
263
|
+
readinessOut(`\n${readiness.message}\n`);
|
|
264
|
+
return readiness.code;
|
|
245
265
|
}
|
|
246
266
|
catch (cause) {
|
|
247
267
|
if (cause instanceof LegacyLayoutRefusedError) {
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import type { ResolutionFacts, ResolutionPlan } from "@deftai/directive-types";
|
|
11
11
|
import { type ClassifySeams, type EngineInstallRunner, type EngineResolution, type LadderFacts, type ReprojectRunner } from "../resolution/index.js";
|
|
12
|
+
import { type AgentHookReadinessResult } from "../verify-env/agent-hook-readiness.js";
|
|
12
13
|
import { type GitLsFiles } from "./gitignore.js";
|
|
13
14
|
import { type InitDepositArgs } from "./init-deposit.js";
|
|
14
15
|
import { type LegacyLayoutDetection } from "./legacy-detect.js";
|
|
@@ -49,6 +50,8 @@ export interface RefreshDepositSeams {
|
|
|
49
50
|
gitHooks?: GitHooksSeams;
|
|
50
51
|
/** #2822: optional seam for trusted-org policy force-on migration. */
|
|
51
52
|
runOrgForceOn?: (projectRoot: string) => void;
|
|
53
|
+
/** Post-deposit functional readiness gate (#3100). */
|
|
54
|
+
evaluateAgentHookReadiness?: (projectRoot: string) => AgentHookReadinessResult;
|
|
52
55
|
}
|
|
53
56
|
/**
|
|
54
57
|
* The four states `directive update` classifies an EXISTING install into BEFORE
|
|
@@ -123,7 +126,12 @@ export interface RefreshSideEffects {
|
|
|
123
126
|
export declare function frameworkRefreshSideEffects(projectDir: string, options?: FrameworkRefreshSideEffectsOptions): RefreshSideEffects;
|
|
124
127
|
export declare function printRefreshSideEffects(io: InitDepositIo, effects: RefreshSideEffects): void;
|
|
125
128
|
export declare function buildVersionSkewNotice(engineVersion: string, contentVersion: string, previousDepositVersion: string | null): string | null;
|
|
126
|
-
export declare function buildUpdateSummaryJson(
|
|
129
|
+
export declare function buildUpdateSummaryJson(input: {
|
|
130
|
+
result: RefreshDepositResult;
|
|
131
|
+
options: RefreshDepositArgs;
|
|
132
|
+
updateState: UpdateState | undefined;
|
|
133
|
+
readiness: AgentHookReadinessResult | undefined;
|
|
134
|
+
}): Record<string, unknown>;
|
|
127
135
|
export declare function printUpdateComplete(result: RefreshDepositResult, io: InitDepositIo, updateState?: UpdateState): void;
|
|
128
136
|
export declare function runRefreshDeposit(args: RefreshDepositArgs, io: InitDepositIo, seams?: RefreshDepositSeams): Promise<RefreshDepositResult>;
|
|
129
137
|
export interface RunRefreshDepositCliOptions extends RefreshDepositArgs {
|
|
@@ -17,12 +17,14 @@ import { prunePythonArtifactsFromDeposit } from "../deposit/python-free.js";
|
|
|
17
17
|
import { resolveInstalledContentRoot } from "../deposit/resolve-content.js";
|
|
18
18
|
import { manifestTagToVersion, parseInstallManifest } from "../doctor/manifest.js";
|
|
19
19
|
import { readCorePackageVersion } from "../engine-version.js";
|
|
20
|
+
import { readLiveGeneration, stampLiveGeneration } from "../freshness/generation.js";
|
|
20
21
|
import { resolveLifecycleRoot } from "../layout/resolve.js";
|
|
21
22
|
import { detectNoDeftDirective, NO_DEFT_DIRECTIVE_DISABLED_MESSAGE, NO_DEFT_DIRECTIVE_FLAG_NAME, NO_DEFT_DIRECTIVE_INCONSISTENT_MESSAGE, NO_DEFT_DIRECTIVE_INCONSISTENT_POLICY, } from "../policy/no-deft-directive.js";
|
|
22
23
|
import { runOrgForceOnMigration } from "../policy/org-force-on-migration.js";
|
|
23
24
|
import { checkLocalEngineIntegrity, classify, ENGINE_PACKAGE, plan, renderGlobalInstall, resolveEngine, } from "../resolution/index.js";
|
|
24
25
|
import { depositOpenClawL2ProductCommands } from "../slash/openclaw-deposit.js";
|
|
25
26
|
import { gitPorcelain } from "../story-ready/git.js";
|
|
27
|
+
import { agentHookReadinessJson, evaluateAgentHookReadiness, evaluateAgentHookReadinessSafely, } from "../verify-env/agent-hook-readiness.js";
|
|
26
28
|
import { removeStaleMigratedFrameworkNarrative } from "../xbrief-migrate/migrate-project.js";
|
|
27
29
|
import { writeAgentHookDeposit } from "./agent-hooks.js";
|
|
28
30
|
import { ensureInitGitignoreLines, isDepositTrackedInGit } from "./gitignore.js";
|
|
@@ -332,9 +334,12 @@ export function buildVersionSkewNotice(engineVersion, contentVersion, previousDe
|
|
|
332
334
|
}
|
|
333
335
|
return null;
|
|
334
336
|
}
|
|
335
|
-
export function buildUpdateSummaryJson(
|
|
337
|
+
export function buildUpdateSummaryJson(input) {
|
|
338
|
+
const { result, options, updateState, readiness } = input;
|
|
336
339
|
return {
|
|
337
|
-
success: true,
|
|
340
|
+
success: readiness ? readiness.code === 0 : true,
|
|
341
|
+
deposit_completed: true,
|
|
342
|
+
...(readiness ? { agent_hook_readiness: agentHookReadinessJson(readiness) } : {}),
|
|
338
343
|
action: "upgrade",
|
|
339
344
|
...(updateState ? { update_state: updateState } : {}),
|
|
340
345
|
version: result.engineVersion,
|
|
@@ -416,6 +421,27 @@ export async function runRefreshDeposit(args, io, seams = {}) {
|
|
|
416
421
|
// VERSION already matches (e.g. pre-#2804 additive deposits). Does not re-stamp.
|
|
417
422
|
await reconcileDepositToContentPackage(deftDir, contentRoot, io);
|
|
418
423
|
migrateLegacyInstallManifest(projectDir, join(deftDir, "VERSION"));
|
|
424
|
+
// #3117: ensure a readable live generation token exists without advancing
|
|
425
|
+
// when the payload did not swap (already-current). stampLiveGeneration is a
|
|
426
|
+
// no-op write when the token already matches (keeps git clean under #2118).
|
|
427
|
+
// If generation is missing or behind VERSION (e.g. prior stamp failed after
|
|
428
|
+
// VERSION rewrite), fail closed — do not report success with a stale token.
|
|
429
|
+
const priorGen = readLiveGeneration(projectDir);
|
|
430
|
+
const generationMatches = priorGen !== null &&
|
|
431
|
+
normalizeVersion(priorGen.contentVersion) === normalizeVersion(contentVersion);
|
|
432
|
+
try {
|
|
433
|
+
stampLiveGeneration(projectDir, {
|
|
434
|
+
contentVersion,
|
|
435
|
+
stampedBy: "directive-update",
|
|
436
|
+
increment: false,
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
catch (err) {
|
|
440
|
+
if (!generationMatches) {
|
|
441
|
+
throw err;
|
|
442
|
+
}
|
|
443
|
+
// Token already matches content; ensure write failure is non-fatal noise.
|
|
444
|
+
}
|
|
419
445
|
}
|
|
420
446
|
else {
|
|
421
447
|
// Full-tree replace (or injected seam). Additive copy is no longer the default.
|
|
@@ -426,12 +452,13 @@ export async function runRefreshDeposit(args, io, seams = {}) {
|
|
|
426
452
|
// additive seams. Throws => no VERSION rewrite (refuse stamp until clean).
|
|
427
453
|
await reconcileDepositToContentPackage(deftDir, contentRoot, io);
|
|
428
454
|
const nowIso = seams.nowIso ?? (() => new Date().toISOString().replace(/\.\d{3}Z$/, "Z"));
|
|
455
|
+
const stampedAt = nowIso();
|
|
429
456
|
const manifestFields = {
|
|
430
457
|
ref: contentVersion.startsWith("v") ? contentVersion : `v${contentVersion}`,
|
|
431
458
|
sha: "content-package",
|
|
432
459
|
tag: contentVersion.startsWith("v") ? contentVersion : `v${contentVersion}`,
|
|
433
460
|
installRoot: CANONICAL_INSTALL_ROOT,
|
|
434
|
-
fetchedAt:
|
|
461
|
+
fetchedAt: stampedAt,
|
|
435
462
|
fetchedBy: "directive-update",
|
|
436
463
|
...(previousManagedBy ? { managedBy: previousManagedBy } : {}),
|
|
437
464
|
};
|
|
@@ -440,6 +467,15 @@ export async function runRefreshDeposit(args, io, seams = {}) {
|
|
|
440
467
|
// .deft/core/VERSION has been rewritten (folded in from install-upgrade so no
|
|
441
468
|
// manifest behavior is lost by the redirect). Best-effort; never fatal.
|
|
442
469
|
migrateLegacyInstallManifest(projectDir, writtenManifestPath);
|
|
470
|
+
// #3117: monotonic live generation MUST advance after a successful payload
|
|
471
|
+
// swap. Suppressing stamp failure would leave a prior bound/live match
|
|
472
|
+
// reporting `current` while the on-disk payload already changed (Greptile P1).
|
|
473
|
+
stampLiveGeneration(projectDir, {
|
|
474
|
+
contentVersion,
|
|
475
|
+
stampedBy: "directive-update",
|
|
476
|
+
increment: true,
|
|
477
|
+
nowIso: stampedAt,
|
|
478
|
+
});
|
|
443
479
|
}
|
|
444
480
|
// #2595: payload freshness and consumer derivative freshness are independent.
|
|
445
481
|
// Always repair these cheap projections, including on the #2118 no-op path.
|
|
@@ -653,17 +689,24 @@ export async function runRefreshDepositCli(options) {
|
|
|
653
689
|
}
|
|
654
690
|
try {
|
|
655
691
|
const result = await runRefreshDeposit(options, io, options.seams);
|
|
692
|
+
const readiness = evaluateAgentHookReadinessSafely(result.projectDir, options.seams?.evaluateAgentHookReadiness ?? evaluateAgentHookReadiness);
|
|
656
693
|
const state = result.alreadyCurrent
|
|
657
694
|
? "current"
|
|
658
695
|
: classification?.state;
|
|
659
696
|
if (options.jsonOut) {
|
|
660
|
-
options.writeOut(`${JSON.stringify(buildUpdateSummaryJson(result, options, state), null, 2)}\n`);
|
|
697
|
+
options.writeOut(`${JSON.stringify(buildUpdateSummaryJson({ result, options, updateState: state, readiness }), null, 2)}\n`);
|
|
661
698
|
printUpdateComplete(result, { printf: options.writeErr }, state);
|
|
662
699
|
}
|
|
663
700
|
else {
|
|
664
701
|
printUpdateComplete(result, io, state);
|
|
665
702
|
}
|
|
666
|
-
|
|
703
|
+
const readinessOut = options.jsonOut
|
|
704
|
+
? options.writeErr
|
|
705
|
+
: readiness.stream === "stderr"
|
|
706
|
+
? options.writeErr
|
|
707
|
+
: options.writeOut;
|
|
708
|
+
readinessOut(`\n${readiness.message}\n`);
|
|
709
|
+
return readiness.code;
|
|
667
710
|
}
|
|
668
711
|
catch (cause) {
|
|
669
712
|
if (cause instanceof LegacyLayoutRefusedError) {
|
|
@@ -507,11 +507,12 @@ function coreGuardWorkflowContent() {
|
|
|
507
507
|
const baseSha = githubActionsExpr("github.event.pull_request.base.sha");
|
|
508
508
|
const headSha = githubActionsExpr("github.event.pull_request.head.sha");
|
|
509
509
|
return ("name: deft-core-guard\n\n" +
|
|
510
|
-
"# Deft framework guard (#1430): a single PR should not mix changes to the\n" +
|
|
511
|
-
"# vendored framework payload (.deft/core/**) with
|
|
512
|
-
"#
|
|
513
|
-
"#
|
|
514
|
-
"#
|
|
510
|
+
"# Deft framework guard (#1430 / #3127): a single PR should not mix changes to the\n" +
|
|
511
|
+
"# vendored framework payload (.deft/core/**) with true application/product files.\n" +
|
|
512
|
+
"# One upgrade PR MAY include deposit + installer-managed paths + package.json pin/lock\n" +
|
|
513
|
+
"# + .deft/GENERATION.json. Framework updates come from `deft update` / install and should\n" +
|
|
514
|
+
"# land without product feature work so reviewers treat the payload as machine-managed.\n" +
|
|
515
|
+
"# Delete this file if you do not want the guard.\n" +
|
|
515
516
|
"on:\n" +
|
|
516
517
|
" pull_request:\n\n" +
|
|
517
518
|
"permissions:\n" +
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parent turn-shape hard-stop for text-repetition hang (FC14 / #3131).
|
|
3
|
+
*
|
|
4
|
+
* Soft skill prose (#2943) alone is insufficient: parents still burn the full
|
|
5
|
+
* output budget on identical progress sentences with zero tool_use after leaf
|
|
6
|
+
* announce. This pure library is the Directive-side machine-checkable gate.
|
|
7
|
+
*
|
|
8
|
+
* Hosts (OpenClaw, swarm parents, review monitors) SHOULD evaluate the parent
|
|
9
|
+
* turn stream mid-generation and hard-stop when `ok` is false.
|
|
10
|
+
*
|
|
11
|
+
* Illegal shape (MUST NOT):
|
|
12
|
+
* N > maxIdenticalWithoutTool (default 2) near-identical assistant sentences
|
|
13
|
+
* (or streaming text chunks) in one turn with no tool_use and no yield.
|
|
14
|
+
*
|
|
15
|
+
* Legal post-announce shapes:
|
|
16
|
+
* 1. Tool batch (ground-truth) then one consolidate
|
|
17
|
+
* 2. sessions_yield / wait without filler
|
|
18
|
+
* 3. One short user answer that is NOT a repeated progress line
|
|
19
|
+
*/
|
|
20
|
+
/** Failure class codes for operator / host recovery surfaces. */
|
|
21
|
+
export type ParentTurnFailClass = "none" | "FC14" | "text-repetition-hang" | "progress-only-no-tool";
|
|
22
|
+
/** Ordered events observed within a single parent turn. */
|
|
23
|
+
export type ParentTurnEvent = {
|
|
24
|
+
kind: "assistant_text";
|
|
25
|
+
text: string;
|
|
26
|
+
} | {
|
|
27
|
+
kind: "tool_use";
|
|
28
|
+
name?: string;
|
|
29
|
+
} | {
|
|
30
|
+
kind: "yield";
|
|
31
|
+
};
|
|
32
|
+
export interface ParentTurnShapeInput {
|
|
33
|
+
/** Ordered events from one parent turn (streaming deltas may be separate). */
|
|
34
|
+
readonly events: readonly ParentTurnEvent[];
|
|
35
|
+
/**
|
|
36
|
+
* When true, multi-sentence progress-only text with zero tools/yield is also
|
|
37
|
+
* illegal even without exact N>2 identity (post-`subagent_announce` policy).
|
|
38
|
+
*/
|
|
39
|
+
readonly afterSubagentAnnounce?: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Max near-identical consecutive (or within-turn) text units allowed without
|
|
42
|
+
* tool_use / yield. Issue contract: MUST NOT emit N>2 → default max = 2.
|
|
43
|
+
*/
|
|
44
|
+
readonly maxIdenticalWithoutTool?: number;
|
|
45
|
+
}
|
|
46
|
+
export interface ParentTurnShapeResult {
|
|
47
|
+
readonly ok: boolean;
|
|
48
|
+
/** Primary fail class (`FC14` aliases `text-repetition-hang`). */
|
|
49
|
+
readonly failClass: ParentTurnFailClass;
|
|
50
|
+
readonly reasons: readonly string[];
|
|
51
|
+
/** Highest count of near-identical text units observed without tool/yield. */
|
|
52
|
+
readonly maxIdenticalCount: number;
|
|
53
|
+
readonly hasToolUse: boolean;
|
|
54
|
+
readonly hasYield: boolean;
|
|
55
|
+
}
|
|
56
|
+
/** Canonical fail class for the hang (#3131 / soft FC14 recurrence of #2943). */
|
|
57
|
+
export declare const PARENT_TURN_FAIL_FC14: "FC14";
|
|
58
|
+
/** Default max identical text units without tool/yield (N>2 is illegal). */
|
|
59
|
+
export declare const DEFAULT_MAX_IDENTICAL_WITHOUT_TOOL = 2;
|
|
60
|
+
/**
|
|
61
|
+
* Normalize assistant text for identity comparison.
|
|
62
|
+
* Collapses whitespace, lowercases, strips common trailing punctuation.
|
|
63
|
+
*/
|
|
64
|
+
export declare function normalizeTurnText(text: string): string;
|
|
65
|
+
/** Split a blob into sentence-like units (periods, newlines, ellipsis). */
|
|
66
|
+
export declare function splitTextUnits(text: string): string[];
|
|
67
|
+
/**
|
|
68
|
+
* True when two normalized strings are near-identical (exact, containment with
|
|
69
|
+
* high length ratio, or high word Jaccard). Short units never match.
|
|
70
|
+
*/
|
|
71
|
+
export declare function isNearIdentical(a: string, b: string): boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Detect when a single blob itself embeds the same sentence many times
|
|
74
|
+
* (model streams one giant assistant message of repeated lines).
|
|
75
|
+
*/
|
|
76
|
+
export declare function countRepeatedUnitsInBlob(text: string): number;
|
|
77
|
+
/**
|
|
78
|
+
* Evaluate whether a parent turn shape is legal under the FC14 hard-stop.
|
|
79
|
+
* Pure / side-effect free — safe for mid-stream host gates and unit tests.
|
|
80
|
+
*/
|
|
81
|
+
export declare function evaluateParentTurnShape(input: ParentTurnShapeInput): ParentTurnShapeResult;
|
|
82
|
+
/** Convenience: true when the turn is an illegal text-repetition hang. */
|
|
83
|
+
export declare function isTextRepetitionHang(input: ParentTurnShapeInput): boolean;
|
|
84
|
+
//# sourceMappingURL=evaluate.d.ts.map
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parent turn-shape hard-stop for text-repetition hang (FC14 / #3131).
|
|
3
|
+
*
|
|
4
|
+
* Soft skill prose (#2943) alone is insufficient: parents still burn the full
|
|
5
|
+
* output budget on identical progress sentences with zero tool_use after leaf
|
|
6
|
+
* announce. This pure library is the Directive-side machine-checkable gate.
|
|
7
|
+
*
|
|
8
|
+
* Hosts (OpenClaw, swarm parents, review monitors) SHOULD evaluate the parent
|
|
9
|
+
* turn stream mid-generation and hard-stop when `ok` is false.
|
|
10
|
+
*
|
|
11
|
+
* Illegal shape (MUST NOT):
|
|
12
|
+
* N > maxIdenticalWithoutTool (default 2) near-identical assistant sentences
|
|
13
|
+
* (or streaming text chunks) in one turn with no tool_use and no yield.
|
|
14
|
+
*
|
|
15
|
+
* Legal post-announce shapes:
|
|
16
|
+
* 1. Tool batch (ground-truth) then one consolidate
|
|
17
|
+
* 2. sessions_yield / wait without filler
|
|
18
|
+
* 3. One short user answer that is NOT a repeated progress line
|
|
19
|
+
*/
|
|
20
|
+
/** Canonical fail class for the hang (#3131 / soft FC14 recurrence of #2943). */
|
|
21
|
+
export const PARENT_TURN_FAIL_FC14 = "FC14";
|
|
22
|
+
/** Default max identical text units without tool/yield (N>2 is illegal). */
|
|
23
|
+
export const DEFAULT_MAX_IDENTICAL_WITHOUT_TOOL = 2;
|
|
24
|
+
/** Minimum length (chars) for a text unit to count toward repetition. */
|
|
25
|
+
const MIN_UNIT_LEN = 12;
|
|
26
|
+
/** Word Jaccard threshold for near-identical (after normalize). */
|
|
27
|
+
const JACCARD_NEAR = 0.9;
|
|
28
|
+
/** Progress-ish tokens that mark filler narration after announce. */
|
|
29
|
+
const PROGRESS_HINT_RE = /\b(checking|checking\s+worktrees|open\s+prs?|next|unfinished|implementing|looking\s+at|will\s+(check|inspect|verify|spawn)|status\s+next|monitor(?:ing)?)\b/i;
|
|
30
|
+
/**
|
|
31
|
+
* Normalize assistant text for identity comparison.
|
|
32
|
+
* Collapses whitespace, lowercases, strips common trailing punctuation.
|
|
33
|
+
*/
|
|
34
|
+
export function normalizeTurnText(text) {
|
|
35
|
+
return text
|
|
36
|
+
.normalize("NFKC")
|
|
37
|
+
.toLowerCase()
|
|
38
|
+
.replace(/\s+/g, " ")
|
|
39
|
+
.replace(/^[\s"'`]+|[\s"'`]+$/g, "")
|
|
40
|
+
.replace(/[.!?…,:;]+$/g, "")
|
|
41
|
+
.trim();
|
|
42
|
+
}
|
|
43
|
+
/** Split a blob into sentence-like units (periods, newlines, ellipsis). */
|
|
44
|
+
export function splitTextUnits(text) {
|
|
45
|
+
const raw = text.replace(/\r\n/g, "\n");
|
|
46
|
+
// Include single newlines so block-formatted repeated progress lines still
|
|
47
|
+
// unitize (FC14 hang class often streams one line per newline, no period).
|
|
48
|
+
// Optional closing quotes after terminal punctuation, with or without space:
|
|
49
|
+
// `"Hello." "Hello."` and `"Hello.""Hello."`.
|
|
50
|
+
const parts = raw
|
|
51
|
+
.split(/(?:\n+|(?<=[.!?…])["']+\s*|(?<=[.!?…])\s+|(?<=\.\.\.)\s+)/)
|
|
52
|
+
.map((p) => p.trim())
|
|
53
|
+
.filter((p) => p.length > 0);
|
|
54
|
+
if (parts.length === 0 && raw.trim().length > 0) {
|
|
55
|
+
return [raw.trim()];
|
|
56
|
+
}
|
|
57
|
+
return parts;
|
|
58
|
+
}
|
|
59
|
+
function wordSet(normalized) {
|
|
60
|
+
const words = normalized.split(/[^a-z0-9_]+/).filter((w) => w.length > 0);
|
|
61
|
+
return new Set(words);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* True when two normalized strings are near-identical (exact, containment with
|
|
65
|
+
* high length ratio, or high word Jaccard). Short units never match.
|
|
66
|
+
*/
|
|
67
|
+
export function isNearIdentical(a, b) {
|
|
68
|
+
const na = normalizeTurnText(a);
|
|
69
|
+
const nb = normalizeTurnText(b);
|
|
70
|
+
if (na.length < MIN_UNIT_LEN || nb.length < MIN_UNIT_LEN)
|
|
71
|
+
return false;
|
|
72
|
+
if (na === nb)
|
|
73
|
+
return true;
|
|
74
|
+
const shorter = na.length <= nb.length ? na : nb;
|
|
75
|
+
const longer = na.length <= nb.length ? nb : na;
|
|
76
|
+
if (longer.includes(shorter) && shorter.length / longer.length >= 0.85) {
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
const sa = wordSet(na);
|
|
80
|
+
const sb = wordSet(nb);
|
|
81
|
+
if (sa.size === 0 || sb.size === 0)
|
|
82
|
+
return false;
|
|
83
|
+
let inter = 0;
|
|
84
|
+
for (const w of sa) {
|
|
85
|
+
if (sb.has(w))
|
|
86
|
+
inter += 1;
|
|
87
|
+
}
|
|
88
|
+
const union = sa.size + sb.size - inter;
|
|
89
|
+
if (union === 0)
|
|
90
|
+
return false;
|
|
91
|
+
return inter / union >= JACCARD_NEAR;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Collect assistant text units from turn events.
|
|
95
|
+
*
|
|
96
|
+
* Streaming hosts often emit word/chunk deltas as separate `assistant_text`
|
|
97
|
+
* events. Coalesce consecutive text events into one blob so fragmented
|
|
98
|
+
* progress sentences reconstruct before split/identity checks (P1 / #3131).
|
|
99
|
+
* Non-text events (tool_use / yield) flush the coalesce buffer.
|
|
100
|
+
*/
|
|
101
|
+
function collectAssistantUnits(events) {
|
|
102
|
+
const units = [];
|
|
103
|
+
const pushUnitsFrom = (text) => {
|
|
104
|
+
for (const u of splitTextUnits(text)) {
|
|
105
|
+
const t = u.trim();
|
|
106
|
+
if (t.length >= MIN_UNIT_LEN)
|
|
107
|
+
units.push(t);
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
let run = "";
|
|
111
|
+
for (const ev of events) {
|
|
112
|
+
if (ev.kind === "assistant_text") {
|
|
113
|
+
const text = typeof ev.text === "string" ? ev.text : "";
|
|
114
|
+
if (!text)
|
|
115
|
+
continue;
|
|
116
|
+
// Sentence-boundary deltas often omit whitespace after `.!?` before the
|
|
117
|
+
// next capital letter. Insert space only at sentence ends — never between
|
|
118
|
+
// arbitrary alnum chunks (subword streams like workt+rees must not gain
|
|
119
|
+
// a false space).
|
|
120
|
+
if (run.length > 0 && /[.!?…]["']?\s*$/.test(run) && !/^\s/.test(text) && !/\s$/.test(run)) {
|
|
121
|
+
run += " ";
|
|
122
|
+
}
|
|
123
|
+
run += text;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
// Non-text event ends the coalesce run (tool/yield boundaries).
|
|
127
|
+
if (run.trim().length > 0)
|
|
128
|
+
pushUnitsFrom(run);
|
|
129
|
+
run = "";
|
|
130
|
+
}
|
|
131
|
+
if (run.trim().length > 0)
|
|
132
|
+
pushUnitsFrom(run);
|
|
133
|
+
return units;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Max connected-component size under near-identity edges (union-find).
|
|
137
|
+
* Captures non-transitive chains of incrementally varied progress lines
|
|
138
|
+
* (A≈B≈C even when A≉C) so FC14 cannot be bypassed by wording drift.
|
|
139
|
+
*/
|
|
140
|
+
function maxNearIdentityComponent(units) {
|
|
141
|
+
const n = units.length;
|
|
142
|
+
if (n === 0)
|
|
143
|
+
return 0;
|
|
144
|
+
// Precompute normalize + word sets once (avoid O(n²) re-normalize/Set alloc).
|
|
145
|
+
const norms = units.map((u) => normalizeTurnText(u));
|
|
146
|
+
const sets = norms.map((n) => wordSet(n));
|
|
147
|
+
const near = (i, j) => {
|
|
148
|
+
const na = norms[i] ?? "";
|
|
149
|
+
const nb = norms[j] ?? "";
|
|
150
|
+
if (na.length < MIN_UNIT_LEN || nb.length < MIN_UNIT_LEN)
|
|
151
|
+
return false;
|
|
152
|
+
if (na === nb)
|
|
153
|
+
return true;
|
|
154
|
+
const shorter = na.length <= nb.length ? na : nb;
|
|
155
|
+
const longer = na.length <= nb.length ? nb : na;
|
|
156
|
+
if (longer.includes(shorter) && shorter.length / longer.length >= 0.85)
|
|
157
|
+
return true;
|
|
158
|
+
const sa = sets[i] ?? new Set();
|
|
159
|
+
const sb = sets[j] ?? new Set();
|
|
160
|
+
if (sa.size === 0 || sb.size === 0)
|
|
161
|
+
return false;
|
|
162
|
+
let inter = 0;
|
|
163
|
+
for (const w of sa) {
|
|
164
|
+
if (sb.has(w))
|
|
165
|
+
inter += 1;
|
|
166
|
+
}
|
|
167
|
+
const union = sa.size + sb.size - inter;
|
|
168
|
+
return union > 0 && inter / union >= JACCARD_NEAR;
|
|
169
|
+
};
|
|
170
|
+
const parent = Array.from({ length: n }, (_, i) => i);
|
|
171
|
+
const find = (i) => {
|
|
172
|
+
let x = i;
|
|
173
|
+
while (parent[x] !== x) {
|
|
174
|
+
parent[x] = parent[parent[x] ?? x] ?? x;
|
|
175
|
+
x = parent[x] ?? x;
|
|
176
|
+
}
|
|
177
|
+
return x;
|
|
178
|
+
};
|
|
179
|
+
const unite = (a, b) => {
|
|
180
|
+
const ra = find(a);
|
|
181
|
+
const rb = find(b);
|
|
182
|
+
if (ra !== rb)
|
|
183
|
+
parent[ra] = rb;
|
|
184
|
+
};
|
|
185
|
+
for (let i = 0; i < n; i++) {
|
|
186
|
+
for (let j = i + 1; j < n; j++) {
|
|
187
|
+
if (near(i, j))
|
|
188
|
+
unite(i, j);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
const sizes = new Map();
|
|
192
|
+
let max = 1;
|
|
193
|
+
for (let i = 0; i < n; i++) {
|
|
194
|
+
const r = find(i);
|
|
195
|
+
const next = (sizes.get(r) ?? 0) + 1;
|
|
196
|
+
sizes.set(r, next);
|
|
197
|
+
if (next > max)
|
|
198
|
+
max = next;
|
|
199
|
+
}
|
|
200
|
+
return max;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Max count of any single near-identical cluster among units.
|
|
204
|
+
* Consecutive runs + exact-normalized frequency (O(n)); for small unit counts
|
|
205
|
+
* also union-find near-identity components so non-consecutive / chain variants
|
|
206
|
+
* cannot bypass FC14 (P1 / #3131).
|
|
207
|
+
*/
|
|
208
|
+
function maxIdenticalCluster(units) {
|
|
209
|
+
if (units.length === 0)
|
|
210
|
+
return 0;
|
|
211
|
+
const norms = units.map((u) => normalizeTurnText(u));
|
|
212
|
+
let maxRun = 1;
|
|
213
|
+
let run = 1;
|
|
214
|
+
for (let i = 1; i < units.length; i++) {
|
|
215
|
+
if (isNearIdentical(units[i] ?? "", units[i - 1] ?? "")) {
|
|
216
|
+
run += 1;
|
|
217
|
+
if (run > maxRun)
|
|
218
|
+
maxRun = run;
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
run = 1;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
// Frequency by exact normalized form (O(n)).
|
|
225
|
+
const freq = new Map();
|
|
226
|
+
let maxFreq = 1;
|
|
227
|
+
for (const n of norms) {
|
|
228
|
+
if (n.length < MIN_UNIT_LEN)
|
|
229
|
+
continue;
|
|
230
|
+
const next = (freq.get(n) ?? 0) + 1;
|
|
231
|
+
freq.set(n, next);
|
|
232
|
+
if (next > maxFreq)
|
|
233
|
+
maxFreq = next;
|
|
234
|
+
}
|
|
235
|
+
// Always cluster near-identity components. Input size is bounded by the
|
|
236
|
+
// zero-tool character hard-stop above, so O(n²) stays finite.
|
|
237
|
+
const maxComponent = maxNearIdentityComponent(units);
|
|
238
|
+
return Math.max(maxRun, maxFreq, maxComponent);
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Detect when a single blob itself embeds the same sentence many times
|
|
242
|
+
* (model streams one giant assistant message of repeated lines).
|
|
243
|
+
*/
|
|
244
|
+
export function countRepeatedUnitsInBlob(text) {
|
|
245
|
+
const units = splitTextUnits(text).filter((u) => u.trim().length >= MIN_UNIT_LEN);
|
|
246
|
+
return maxIdenticalCluster(units);
|
|
247
|
+
}
|
|
248
|
+
function looksLikeProgressOnly(units) {
|
|
249
|
+
if (units.length < 2)
|
|
250
|
+
return false;
|
|
251
|
+
let progressHits = 0;
|
|
252
|
+
for (const u of units) {
|
|
253
|
+
if (PROGRESS_HINT_RE.test(u))
|
|
254
|
+
progressHits += 1;
|
|
255
|
+
}
|
|
256
|
+
// Majority of multi-sentence text is progress narration.
|
|
257
|
+
return progressHits >= Math.ceil(units.length / 2);
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Evaluate whether a parent turn shape is legal under the FC14 hard-stop.
|
|
261
|
+
* Pure / side-effect free — safe for mid-stream host gates and unit tests.
|
|
262
|
+
*/
|
|
263
|
+
export function evaluateParentTurnShape(input) {
|
|
264
|
+
const events = input.events ?? [];
|
|
265
|
+
const maxAllowed = input.maxIdenticalWithoutTool ?? DEFAULT_MAX_IDENTICAL_WITHOUT_TOOL;
|
|
266
|
+
let hasToolUse = false;
|
|
267
|
+
let hasYield = false;
|
|
268
|
+
for (const ev of events) {
|
|
269
|
+
if (ev.kind === "tool_use")
|
|
270
|
+
hasToolUse = true;
|
|
271
|
+
if (ev.kind === "yield")
|
|
272
|
+
hasYield = true;
|
|
273
|
+
}
|
|
274
|
+
// Tool or yield greases the turn: repetition hard-stop does not fire.
|
|
275
|
+
if (hasToolUse || hasYield) {
|
|
276
|
+
return {
|
|
277
|
+
ok: true,
|
|
278
|
+
failClass: "none",
|
|
279
|
+
reasons: [],
|
|
280
|
+
maxIdenticalCount: 0,
|
|
281
|
+
hasToolUse,
|
|
282
|
+
hasYield,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
// Fail closed on unbounded zero-tool text (output-budget burn / DoS class).
|
|
286
|
+
const ZERO_TOOL_CHAR_CAP = 50_000;
|
|
287
|
+
let zeroToolChars = 0;
|
|
288
|
+
for (const ev of events) {
|
|
289
|
+
if (ev.kind === "assistant_text" && typeof ev.text === "string") {
|
|
290
|
+
zeroToolChars += ev.text.length;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
if (zeroToolChars > ZERO_TOOL_CHAR_CAP) {
|
|
294
|
+
return {
|
|
295
|
+
ok: false,
|
|
296
|
+
failClass: "FC14",
|
|
297
|
+
reasons: [
|
|
298
|
+
`FC14 text-repetition-hang: zero-tool assistant text exceeds ${ZERO_TOOL_CHAR_CAP} chars ` +
|
|
299
|
+
`(${zeroToolChars}) without tool_use/yield — hard-stop budget burn. Refs #3131 / #2943.`,
|
|
300
|
+
],
|
|
301
|
+
maxIdenticalCount: Math.ceil(zeroToolChars / MIN_UNIT_LEN),
|
|
302
|
+
hasToolUse,
|
|
303
|
+
hasYield,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
const units = collectAssistantUnits(events);
|
|
307
|
+
// Also fold in whole-blob repetition for single giant text events.
|
|
308
|
+
let maxIdenticalCount = maxIdenticalCluster(units);
|
|
309
|
+
for (const ev of events) {
|
|
310
|
+
if (ev.kind === "assistant_text" && typeof ev.text === "string") {
|
|
311
|
+
maxIdenticalCount = Math.max(maxIdenticalCount, countRepeatedUnitsInBlob(ev.text));
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
const reasons = [];
|
|
315
|
+
let failClass = "none";
|
|
316
|
+
if (maxIdenticalCount > maxAllowed) {
|
|
317
|
+
failClass = "FC14";
|
|
318
|
+
reasons.push(`FC14 text-repetition-hang: ${maxIdenticalCount} near-identical assistant text units ` +
|
|
319
|
+
`with zero tool_use/yield (max allowed ${maxAllowed}; N>${maxAllowed} is illegal). ` +
|
|
320
|
+
`After subagent announce, emit a tool-first ground-truth batch, sessions_yield, ` +
|
|
321
|
+
`or one short non-repeated answer — not repeated progress lines. Refs #3131 / #2943.`);
|
|
322
|
+
}
|
|
323
|
+
// Post-announce: multi-sentence progress-only with zero tools is also illegal
|
|
324
|
+
// even when sentences are not exact clones (soft-only #2943 recurrence class).
|
|
325
|
+
// Threshold is >=2 units (N>1 multi-sentence) — exactly two progress lines
|
|
326
|
+
// must not bypass the gate.
|
|
327
|
+
if (input.afterSubagentAnnounce &&
|
|
328
|
+
units.length >= 2 &&
|
|
329
|
+
looksLikeProgressOnly(units) &&
|
|
330
|
+
failClass === "none") {
|
|
331
|
+
failClass = "progress-only-no-tool";
|
|
332
|
+
reasons.push(`progress-only-no-tool: post-subagent-announce turn has ${units.length} assistant ` +
|
|
333
|
+
`text units with progress narration and zero tool_use/yield. MUST tool-first or yield. Refs #3131 / #2943.`);
|
|
334
|
+
}
|
|
335
|
+
// Alias surface: FC14 and text-repetition-hang name the same hang class.
|
|
336
|
+
if (failClass === "FC14") {
|
|
337
|
+
// Keep failClass as FC14; callers may also match text-repetition-hang via reasons.
|
|
338
|
+
}
|
|
339
|
+
return {
|
|
340
|
+
ok: failClass === "none",
|
|
341
|
+
failClass,
|
|
342
|
+
reasons,
|
|
343
|
+
maxIdenticalCount,
|
|
344
|
+
hasToolUse,
|
|
345
|
+
hasYield,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
/** Convenience: true when the turn is an illegal text-repetition hang. */
|
|
349
|
+
export function isTextRepetitionHang(input) {
|
|
350
|
+
const r = evaluateParentTurnShape(input);
|
|
351
|
+
return r.failClass === "FC14" || r.failClass === "text-repetition-hang";
|
|
352
|
+
}
|
|
353
|
+
//# sourceMappingURL=evaluate.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parent turn-shape hard-stop (FC14 / #3131).
|
|
3
|
+
*
|
|
4
|
+
* Machine-checkable gate so soft skill prose is not the sole mitigation for
|
|
5
|
+
* the OpenClaw parent text-repetition hang after leaf announce.
|
|
6
|
+
*/
|
|
7
|
+
export { countRepeatedUnitsInBlob, DEFAULT_MAX_IDENTICAL_WITHOUT_TOOL, evaluateParentTurnShape, isNearIdentical, isTextRepetitionHang, normalizeTurnText, PARENT_TURN_FAIL_FC14, type ParentTurnEvent, type ParentTurnFailClass, type ParentTurnShapeInput, type ParentTurnShapeResult, splitTextUnits, } from "./evaluate.js";
|
|
8
|
+
//# sourceMappingURL=index.d.ts.map
|