@juzser/blacksmith 0.4.0 → 0.5.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/.claude/agents/{auditor.md → bs-auditor.md} +5 -5
- package/.claude/agents/{coder.md → bs-coder.md} +8 -7
- package/.claude/agents/{grader.md → bs-grader.md} +4 -4
- package/.claude/agents/{merger.md → bs-merger.md} +4 -4
- package/.claude/agents/{planner.md → bs-planner.md} +20 -6
- package/.claude/agents/{researcher.md → bs-researcher.md} +6 -5
- package/.claude/agents/{reviewer.md → bs-reviewer.md} +13 -5
- package/.claude/agents/{scribe.md → bs-scribe.md} +11 -2
- package/.claude/agents/{security-reviewer.md → bs-security-reviewer.md} +6 -6
- package/.claude/agents/{spec-reviewer.md → bs-spec-reviewer.md} +15 -10
- package/.claude/agents/{tester.md → bs-tester.md} +40 -12
- package/.claude/agents/bs-uiux.md +257 -0
- package/.claude/agents/{verifier.md → bs-verifier.md} +4 -4
- package/.claude/agents/{wave-runner.md → bs-wave-runner.md} +14 -5
- package/.claude/skills/bs/SKILL.md +99 -21
- package/.claude/skills/bs/audit.md +34 -27
- package/.claude/skills/bs/dispatch.md +100 -50
- package/.claude/skills/bs/lessons.md +28 -14
- package/.claude/skills/bs/mcp.md +4 -4
- package/.claude/skills/bs/new.md +3 -3
- package/.claude/skills/bs/plan.md +27 -18
- package/.claude/skills/bs/report.md +11 -11
- package/.claude/skills/bs/run.md +181 -49
- package/.claude/skills/bs/status.md +14 -7
- package/.claude/skills/bs/ui.md +21 -5
- package/.claude/skills/bs/waivers.md +78 -2
- package/.claude/skills/bs/wave.md +109 -53
- package/README.md +53 -32
- package/factory/orchestrator/dist/agentNames.js +43 -0
- package/factory/orchestrator/dist/agents-registry.js +11 -4
- package/factory/orchestrator/dist/agentsSync.js +48 -14
- package/factory/orchestrator/dist/artifacts.js +31 -0
- package/factory/orchestrator/dist/attribution.js +4 -0
- package/factory/orchestrator/dist/audit.js +125 -17
- package/factory/orchestrator/dist/budgetAlarm.js +132 -37
- package/factory/orchestrator/dist/budgets.js +302 -84
- package/factory/orchestrator/dist/claims.js +9 -19
- package/factory/orchestrator/dist/cli.js +790 -100
- package/factory/orchestrator/dist/cliName.js +27 -0
- package/factory/orchestrator/dist/crosscheck.js +136 -8
- package/factory/orchestrator/dist/daemon.js +154 -36
- package/factory/orchestrator/dist/db/projector.js +278 -29
- package/factory/orchestrator/dist/db/queries.js +1831 -94
- package/factory/orchestrator/dist/db/schema.js +57 -4
- package/factory/orchestrator/dist/declaredArtifactLine.js +18 -0
- package/factory/orchestrator/dist/delegation.js +86 -19
- package/factory/orchestrator/dist/diffstat.js +15 -8
- package/factory/orchestrator/dist/dispatchLint.js +13 -37
- package/factory/orchestrator/dist/effort.js +14 -2
- package/factory/orchestrator/dist/effortTiers.js +13 -0
- package/factory/orchestrator/dist/env.js +26 -0
- package/factory/orchestrator/dist/epic.js +323 -38
- package/factory/orchestrator/dist/epicBudget.js +93 -0
- package/factory/orchestrator/dist/epicWidth.js +22 -5
- package/factory/orchestrator/dist/errorIssues.js +10 -4
- package/factory/orchestrator/dist/escalation.js +2 -2
- package/factory/orchestrator/dist/events.js +105 -3
- package/factory/orchestrator/dist/feedback.js +169 -0
- package/factory/orchestrator/dist/findingContext.js +26 -0
- package/factory/orchestrator/dist/findings.js +83 -12
- package/factory/orchestrator/dist/gate.js +120 -10
- package/factory/orchestrator/dist/goalCheck.js +2 -2
- package/factory/orchestrator/dist/harness.js +25 -17
- package/factory/orchestrator/dist/hookDecision.js +92 -3
- package/factory/orchestrator/dist/integration.js +9 -7
- package/factory/orchestrator/dist/judgePreflight.js +25 -4
- package/factory/orchestrator/dist/judgeRoles.js +47 -0
- package/factory/orchestrator/dist/judgeStopHook.js +90 -32
- package/factory/orchestrator/dist/judges.js +419 -21
- package/factory/orchestrator/dist/lessons.js +190 -11
- package/factory/orchestrator/dist/mcp.js +6 -6
- package/factory/orchestrator/dist/paths.js +10 -7
- package/factory/orchestrator/dist/plan.js +269 -33
- package/factory/orchestrator/dist/planQuorum.js +5 -2
- package/factory/orchestrator/dist/policy.js +1275 -30
- package/factory/orchestrator/dist/projectName.js +25 -0
- package/factory/orchestrator/dist/promptCapture.js +306 -0
- package/factory/orchestrator/dist/promptHook.js +33 -0
- package/factory/orchestrator/dist/promptImport.js +281 -0
- package/factory/orchestrator/dist/prompts.js +13 -0
- package/factory/orchestrator/dist/providers/schema-validate.js +2 -2
- package/factory/orchestrator/dist/providers/types.js +1 -1
- package/factory/orchestrator/dist/queue.js +4 -4
- package/factory/orchestrator/dist/quorum.js +18 -5
- package/factory/orchestrator/dist/results.js +25 -7
- package/factory/orchestrator/dist/roadmap.js +10 -5
- package/factory/orchestrator/dist/run-cli.js +15 -10
- package/factory/orchestrator/dist/runner.js +2 -2
- package/factory/orchestrator/dist/schemas.js +56 -8
- package/factory/orchestrator/dist/severity.js +8 -4
- package/factory/orchestrator/dist/spec.js +177 -13
- package/factory/orchestrator/dist/specChange.js +13 -9
- package/factory/orchestrator/dist/stack.js +6 -6
- package/factory/orchestrator/dist/storeRoot.js +90 -0
- package/factory/orchestrator/dist/symbols.js +261 -6
- package/factory/orchestrator/dist/taskEvents.js +179 -27
- package/factory/orchestrator/dist/taskStatus.js +14 -0
- package/factory/orchestrator/dist/testerAudit.js +31 -12
- package/factory/orchestrator/dist/testgate.js +16 -13
- package/factory/orchestrator/dist/uiuxGate.js +83 -0
- package/factory/orchestrator/dist/usage.js +97 -40
- package/factory/orchestrator/dist/waivers.js +39 -5
- package/factory/orchestrator/dist/waveBudget.js +2 -0
- package/factory/orchestrator/dist/waveConcurrency.js +143 -7
- package/factory/orchestrator/dist/workroot.js +1 -1
- package/factory/orchestrator/dist/worktree.js +61 -19
- package/factory/orchestrator/drizzle/0013_operator_feedback.sql +19 -0
- package/factory/orchestrator/drizzle/0014_misty_madripoor.sql +2 -0
- package/factory/orchestrator/drizzle/0015_gifted_omega_flight.sql +1 -0
- package/factory/orchestrator/drizzle/0016_milestones_project_default.sql +30 -0
- package/factory/orchestrator/drizzle/0017_events_raw_cli_session_id.sql +2 -0
- package/factory/orchestrator/drizzle/meta/_journal.json +35 -0
- package/factory/policies/budgets.yml +123 -33
- package/factory/policies/crosscheck.yml +51 -13
- package/factory/policies/delegation.yml +3 -3
- package/factory/policies/effort.yml +18 -10
- package/factory/policies/guardrails.yml +8 -6
- package/factory/policies/harness.yml +5 -5
- package/factory/policies/lessons.md +53 -2
- package/factory/policies/scheduler.yml +4 -4
- package/factory/policies/sensitive-paths.yml +1 -1
- package/factory/policies/severity.yml +13 -1
- package/factory/policies/stack.yml +6 -6
- package/factory/policies/taxonomy.yml +9 -3
- package/factory/policies/worktree.yml +15 -9
- package/factory/specs/roadmap.md +7 -7
- package/factory/specs/schema/event.schema.json +8 -1
- package/factory/specs/schema/result.schema.json +21 -8
- package/factory/specs/schema/task-spec.schema.json +12 -0
- package/package.json +16 -12
- package/.claude/agents/uiux.md +0 -133
|
@@ -539,6 +539,250 @@ function findFromSpecifier(masked, from, to) {
|
|
|
539
539
|
function normalizeSignature(text) {
|
|
540
540
|
return text.trim().replace(/\s+/g, ' ');
|
|
541
541
|
}
|
|
542
|
+
/** Type words after which a `{` opens an object type, as it does after `:`. */
|
|
543
|
+
const TYPE_OPERATORS = new Set([
|
|
544
|
+
'is',
|
|
545
|
+
'extends',
|
|
546
|
+
'keyof',
|
|
547
|
+
'readonly',
|
|
548
|
+
'new',
|
|
549
|
+
'abstract',
|
|
550
|
+
]);
|
|
551
|
+
/**
|
|
552
|
+
* Whether the group `open`…`close` reads as a function type's parameters, as
|
|
553
|
+
* TypeScript decides it: empty, a rest parameter, or a name or binding pattern
|
|
554
|
+
* followed by `:`, `,`, `?`, `=` or the `)`. `(A | B)` is a parenthesised type.
|
|
555
|
+
*/
|
|
556
|
+
function isTypeParameterList(masked, open, close) {
|
|
557
|
+
const first = skipSpace(masked, open + 1);
|
|
558
|
+
if (first === close || masked.startsWith('...', first))
|
|
559
|
+
return true;
|
|
560
|
+
const ch = masked.charAt(first);
|
|
561
|
+
let after = first + readIdent(masked, first).length;
|
|
562
|
+
if (ch === '{' || ch === '[')
|
|
563
|
+
after = matchBracket(masked, first, close) + 1;
|
|
564
|
+
return after > first && ':,?=)'.includes(masked.charAt(skipSpace(masked, after)));
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* The index just past the template literal opening at `open`. Its text is
|
|
568
|
+
* already blanked, so the next backtick closes it, and a `${` interpolation
|
|
569
|
+
* is stepped over as a bracket group. An unclosed one runs to `limit`.
|
|
570
|
+
*/
|
|
571
|
+
function skipTemplate(masked, open, limit) {
|
|
572
|
+
let i = open + 1;
|
|
573
|
+
while (i < limit) {
|
|
574
|
+
const ch = masked.charAt(i);
|
|
575
|
+
if (ch === '`')
|
|
576
|
+
return i + 1;
|
|
577
|
+
if (ch === '$' && masked.charAt(i + 1) === '{') {
|
|
578
|
+
const close = matchBracket(masked, i + 1, limit);
|
|
579
|
+
if (close === -1)
|
|
580
|
+
return limit;
|
|
581
|
+
i = close + 1;
|
|
582
|
+
}
|
|
583
|
+
else
|
|
584
|
+
i += 1;
|
|
585
|
+
}
|
|
586
|
+
return limit;
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Walk a return-type annotation (`from` is just past its `:`) and answer where
|
|
590
|
+
* it stops: the index of the function body's `{`, the index of the `=>`,
|
|
591
|
+
* `limit` when the type runs to it, or -1 when what follows the `:` is not a
|
|
592
|
+
* return type at all (a ternary branch, say). A `{` directly after `:`, `|`,
|
|
593
|
+
* `&`, `<`, `,`, `?`, a word in TYPE_OPERATORS or a function type's `=>` is an
|
|
594
|
+
* object type; a template literal type is skipped whole.
|
|
595
|
+
*/
|
|
596
|
+
function skipReturnType(masked, from, limit) {
|
|
597
|
+
let prev = ':';
|
|
598
|
+
let angle = 0;
|
|
599
|
+
let i = from;
|
|
600
|
+
while (i < limit) {
|
|
601
|
+
const ch = masked.charAt(i);
|
|
602
|
+
if (/\s/.test(ch)) {
|
|
603
|
+
i += 1;
|
|
604
|
+
continue;
|
|
605
|
+
}
|
|
606
|
+
if (ch === '`') {
|
|
607
|
+
// A template literal type is skipped whole: its `${…}` braces are not a body.
|
|
608
|
+
i = skipTemplate(masked, i, limit);
|
|
609
|
+
prev = 'a';
|
|
610
|
+
continue;
|
|
611
|
+
}
|
|
612
|
+
if (ch === '=' && masked.charAt(i + 1) === '>')
|
|
613
|
+
return i;
|
|
614
|
+
if (ch === '{' && !':|&<,?'.includes(prev))
|
|
615
|
+
return i;
|
|
616
|
+
if (ch === '{' || ch === '(' || ch === '[') {
|
|
617
|
+
const close = matchBracket(masked, i, limit);
|
|
618
|
+
if (close === -1)
|
|
619
|
+
return -1;
|
|
620
|
+
const arrow = skipSpace(masked, close + 1);
|
|
621
|
+
const functionType = ch === '(' &&
|
|
622
|
+
':|&<,?>'.includes(prev) &&
|
|
623
|
+
masked.startsWith('=>', arrow) &&
|
|
624
|
+
isTypeParameterList(masked, i, close);
|
|
625
|
+
// A function type's `=>` is not the arrow function's; its return type follows.
|
|
626
|
+
i = functionType ? arrow + 2 : close + 1;
|
|
627
|
+
prev = functionType ? ':' : ')';
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
630
|
+
if (isIdentStart(ch)) {
|
|
631
|
+
const word = readIdent(masked, i);
|
|
632
|
+
prev = prev !== '.' && TYPE_OPERATORS.has(word) ? ':' : 'a';
|
|
633
|
+
i += word.length;
|
|
634
|
+
continue;
|
|
635
|
+
}
|
|
636
|
+
if (ch === '<')
|
|
637
|
+
angle += 1;
|
|
638
|
+
else if (ch === '>')
|
|
639
|
+
angle -= 1;
|
|
640
|
+
else if (ch === ')' || ch === ']' || ch === '}' || ch === ';')
|
|
641
|
+
return -1;
|
|
642
|
+
else if (ch === ',' && angle <= 0)
|
|
643
|
+
return -1;
|
|
644
|
+
prev = ch;
|
|
645
|
+
i += 1;
|
|
646
|
+
}
|
|
647
|
+
return limit;
|
|
648
|
+
}
|
|
649
|
+
/**
|
|
650
|
+
* Whether the call opening at `open` is a class heritage, `extends mixin(Base) {`,
|
|
651
|
+
* whose `{` is a class body. It reads back from `open` only as far as the
|
|
652
|
+
* heritage name and the `extends` before it, never to `start`.
|
|
653
|
+
*/
|
|
654
|
+
function isHeritageCall(masked, start, open) {
|
|
655
|
+
let i = open;
|
|
656
|
+
while (i > start && /\s/.test(masked.charAt(i - 1)))
|
|
657
|
+
i -= 1;
|
|
658
|
+
const nameEnd = i;
|
|
659
|
+
while (i > start && /[\w$.]/.test(masked.charAt(i - 1)))
|
|
660
|
+
i -= 1;
|
|
661
|
+
const nameStart = i;
|
|
662
|
+
while (i > start && /\s/.test(masked.charAt(i - 1)))
|
|
663
|
+
i -= 1;
|
|
664
|
+
const keyword = i - 'extends'.length;
|
|
665
|
+
return (nameStart < nameEnd &&
|
|
666
|
+
i < nameStart &&
|
|
667
|
+
keyword >= start &&
|
|
668
|
+
masked.startsWith('extends', keyword) &&
|
|
669
|
+
(keyword === start || !/\w/.test(masked.charAt(keyword - 1))));
|
|
670
|
+
}
|
|
671
|
+
/** Words after `export default` that make it a declaration, not an expression. */
|
|
672
|
+
const DEFAULT_DECLARATIONS = new Set([
|
|
673
|
+
'function',
|
|
674
|
+
'class',
|
|
675
|
+
'interface',
|
|
676
|
+
'abstract',
|
|
677
|
+
'async',
|
|
678
|
+
]);
|
|
679
|
+
/**
|
|
680
|
+
* Where the clause's value starts — just past the `=` of `export const|let|var`
|
|
681
|
+
* or past `default` in `export default <expression>` — or -1 for any other
|
|
682
|
+
* clause. Only a value can hold a function body: a declaration's own body
|
|
683
|
+
* lies past the clause, and an annotation, a type alias, an interface or a
|
|
684
|
+
* `declare` is all types, where a `{` after `=>` is an object type.
|
|
685
|
+
*/
|
|
686
|
+
function valueStart(masked, from, end) {
|
|
687
|
+
const at = skipSpace(masked, from);
|
|
688
|
+
const word = readIdent(masked, at);
|
|
689
|
+
if (word === 'default') {
|
|
690
|
+
const next = readIdent(masked, skipSpace(masked, at + word.length));
|
|
691
|
+
return DEFAULT_DECLARATIONS.has(next) ? -1 : at + word.length;
|
|
692
|
+
}
|
|
693
|
+
if (word !== 'const' && word !== 'let' && word !== 'var')
|
|
694
|
+
return -1;
|
|
695
|
+
let depth = 0;
|
|
696
|
+
for (let i = at + word.length; i < end; i += 1) {
|
|
697
|
+
const ch = masked.charAt(i);
|
|
698
|
+
if (ch === '=' && masked.charAt(i + 1) === '>')
|
|
699
|
+
i += 1;
|
|
700
|
+
else if ('([{<'.includes(ch))
|
|
701
|
+
depth += 1;
|
|
702
|
+
else if (')]}>'.includes(ch))
|
|
703
|
+
depth -= 1;
|
|
704
|
+
else if (ch === '=' && depth === 0)
|
|
705
|
+
return i + 1;
|
|
706
|
+
}
|
|
707
|
+
return -1;
|
|
708
|
+
}
|
|
709
|
+
/** What a function body reads as in a signature. */
|
|
710
|
+
const BODY_PLACEHOLDER = '{…}';
|
|
711
|
+
/**
|
|
712
|
+
* The value text an importer can see (`start` is from valueStart): every
|
|
713
|
+
* function body in it becomes a placeholder, so an edit inside a body is not a
|
|
714
|
+
* signature edit, while what follows a body (a `memo` comparator, an `as` cast,
|
|
715
|
+
* a chained call) still is. A body is the `{` right after a parameter list,
|
|
716
|
+
* after its return type, or after `=>`; any other brace is text. The scan steps
|
|
717
|
+
* over a parameter list and its return type once it finds the body or the
|
|
718
|
+
* `=>` after them, or when they run to the `{` that ends the clause (that body
|
|
719
|
+
* lies past `end`); any other parenthesised group, a call or a ternary branch,
|
|
720
|
+
* it reads into. Nothing past `end` is read, and an unmatched bracket keeps the
|
|
721
|
+
* text whole.
|
|
722
|
+
*
|
|
723
|
+
* Known limits, both rare: a function type with an object return inside a type
|
|
724
|
+
* argument or an `as` type within the value, `memo(forwardRef<R, (a: A) => { b: B }>(…))`
|
|
725
|
+
* or `memo(C as (p: P) => { a: A })`, reads its object type as a body; and a
|
|
726
|
+
* heritage that is not `extends name(…) {`, such as `extends mixin(A)(B) {`,
|
|
727
|
+
* `extends (c ? A : B) {` or `extends mixin<T>(Base) {`, has its class body
|
|
728
|
+
* read as a function body.
|
|
729
|
+
*/
|
|
730
|
+
function signatureText(source, masked, start, end) {
|
|
731
|
+
let text = '';
|
|
732
|
+
let kept = start;
|
|
733
|
+
let i = start;
|
|
734
|
+
while (i < end) {
|
|
735
|
+
const ch = masked.charAt(i);
|
|
736
|
+
let body = -1;
|
|
737
|
+
if (ch === '=' && masked.charAt(i + 1) === '>') {
|
|
738
|
+
const next = skipSpace(masked, i + 2);
|
|
739
|
+
if (next >= end || masked.charAt(next) !== '{') {
|
|
740
|
+
i = next;
|
|
741
|
+
continue;
|
|
742
|
+
}
|
|
743
|
+
body = next;
|
|
744
|
+
}
|
|
745
|
+
else if (ch === '(') {
|
|
746
|
+
const close = matchBracket(masked, i, end);
|
|
747
|
+
if (close === -1)
|
|
748
|
+
return source.slice(start, end);
|
|
749
|
+
const after = skipSpace(masked, close + 1);
|
|
750
|
+
const next = after < end ? masked.charAt(after) : '';
|
|
751
|
+
const stop = next === ':' ? skipReturnType(masked, after + 1, end) : after;
|
|
752
|
+
if (stop >= end && masked.charAt(end) === '{') {
|
|
753
|
+
// The `{` that ends the clause is this function's body: keep its parameters whole.
|
|
754
|
+
i = end;
|
|
755
|
+
continue;
|
|
756
|
+
}
|
|
757
|
+
if (next === '{' && !isHeritageCall(masked, start, i))
|
|
758
|
+
body = after;
|
|
759
|
+
else if (next === '=' && masked.charAt(after + 1) === '>') {
|
|
760
|
+
i = after;
|
|
761
|
+
continue;
|
|
762
|
+
}
|
|
763
|
+
else if (next === ':' && stop !== -1 && stop < end) {
|
|
764
|
+
if (masked.charAt(stop) === '{')
|
|
765
|
+
body = stop;
|
|
766
|
+
else {
|
|
767
|
+
i = stop;
|
|
768
|
+
continue;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
if (body === -1) {
|
|
773
|
+
// A call, a group or plain text: look inside it.
|
|
774
|
+
i += 1;
|
|
775
|
+
continue;
|
|
776
|
+
}
|
|
777
|
+
const close = matchBracket(masked, body, end);
|
|
778
|
+
if (close === -1)
|
|
779
|
+
return source.slice(start, end);
|
|
780
|
+
text += source.slice(kept, body) + BODY_PLACEHOLDER;
|
|
781
|
+
kept = close + 1;
|
|
782
|
+
i = kept;
|
|
783
|
+
}
|
|
784
|
+
return text + source.slice(kept, end);
|
|
785
|
+
}
|
|
542
786
|
/**
|
|
543
787
|
* Record the declaration text against every export the clause introduced.
|
|
544
788
|
* Wrapping is what keeps this honest: the clause parsers below push export
|
|
@@ -548,7 +792,15 @@ function normalizeSignature(text) {
|
|
|
548
792
|
function readClauseFacts(source, masked, start, clause, keyword, facts) {
|
|
549
793
|
const before = facts.exports.length;
|
|
550
794
|
readClauseExports(source, masked, start, clause, keyword, facts);
|
|
551
|
-
|
|
795
|
+
// An import introduces no export, so it has no signature to read.
|
|
796
|
+
if (facts.exports.length === before)
|
|
797
|
+
return;
|
|
798
|
+
const end = clause.textEnd;
|
|
799
|
+
const value = valueStart(masked, start + keyword.length, end);
|
|
800
|
+
const text = value === -1
|
|
801
|
+
? source.slice(start, end)
|
|
802
|
+
: source.slice(start, value) + signatureText(source, masked, value, end);
|
|
803
|
+
const signature = normalizeSignature(text);
|
|
552
804
|
for (let i = before; i < facts.exports.length; i += 1) {
|
|
553
805
|
const name = facts.exports[i];
|
|
554
806
|
if (name !== undefined)
|
|
@@ -664,13 +916,16 @@ function readStatementClauses(source, masked, facts) {
|
|
|
664
916
|
i += 1;
|
|
665
917
|
}
|
|
666
918
|
}
|
|
667
|
-
|
|
919
|
+
/** Index of the bracket closing the one at `open`, before `limit`; -1 when unclosed. */
|
|
920
|
+
function matchBracket(masked, open, limit = masked.length) {
|
|
921
|
+
const opener = masked.charAt(open);
|
|
922
|
+
const closer = opener === '{' ? '}' : opener === '[' ? ']' : ')';
|
|
668
923
|
let depth = 0;
|
|
669
|
-
for (let i = open; i <
|
|
924
|
+
for (let i = open; i < limit; i += 1) {
|
|
670
925
|
const ch = masked.charAt(i);
|
|
671
|
-
if (ch ===
|
|
926
|
+
if (ch === opener)
|
|
672
927
|
depth += 1;
|
|
673
|
-
else if (ch ===
|
|
928
|
+
else if (ch === closer) {
|
|
674
929
|
depth -= 1;
|
|
675
930
|
if (depth === 0)
|
|
676
931
|
return i;
|
|
@@ -683,7 +938,7 @@ function readDynamicImports(source, masked, facts) {
|
|
|
683
938
|
let match = pattern.exec(masked);
|
|
684
939
|
while (match !== null) {
|
|
685
940
|
const open = match.index + match[0].length - 1;
|
|
686
|
-
const close =
|
|
941
|
+
const close = matchBracket(masked, open);
|
|
687
942
|
const inner = close === -1 ? masked.length : close;
|
|
688
943
|
const quoteStart = skipSpace(masked, open + 1);
|
|
689
944
|
const quote = masked.charAt(quoteStart);
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { appendEdge, appendEvent, readLineageEvents, } from './events.js';
|
|
2
2
|
import { resolveTaskId } from './plan.js';
|
|
3
|
-
import {
|
|
3
|
+
import { epicOfTaskId, taskIdsMatch } from './taskId.js';
|
|
4
|
+
import { CLOSED_TO_FURTHER_WORK } from './taskStatus.js';
|
|
5
|
+
import { BRANCH_PREFIX, epicBranchPrefix, taskBranchName } from './worktree.js';
|
|
4
6
|
function envelope(ctx, eventType, payload, taskId) {
|
|
5
7
|
return {
|
|
6
8
|
session_id: ctx.sessionId,
|
|
@@ -22,7 +24,7 @@ function envelope(ctx, eventType, payload, taskId) {
|
|
|
22
24
|
* emitTasksAdded, which used it for the epic id and the task specs and let the
|
|
23
25
|
* project fall on the floor. Every event of a demo-rpg run therefore landed
|
|
24
26
|
* with no project at all, and db/queries.ts's projectOf() resolves an absent
|
|
25
|
-
* one to the default: a whole epic filed under
|
|
27
|
+
* one to the default: a whole epic filed under blacksmith, missing from the
|
|
26
28
|
* board of the project it was actually part of.
|
|
27
29
|
*
|
|
28
30
|
* The plan is the fallback and not an override, because a caller holding a
|
|
@@ -34,7 +36,7 @@ function planScoped(ctx, plan) {
|
|
|
34
36
|
const project = ctx.project ?? plan.project;
|
|
35
37
|
return project === undefined ? ctx : { ...ctx, project };
|
|
36
38
|
}
|
|
37
|
-
function addedPayload(plan, task) {
|
|
39
|
+
function addedPayload(plan, task, branchPrefix) {
|
|
38
40
|
const budget = task.budget;
|
|
39
41
|
// A task spec is a `Record<string, unknown>`, so its `epic_id` is only a
|
|
40
42
|
// string by convention. `taskBranchName` needs a real one, and a task
|
|
@@ -47,14 +49,20 @@ function addedPayload(plan, task) {
|
|
|
47
49
|
task_status: task.task_status,
|
|
48
50
|
plan_version: task.plan_version,
|
|
49
51
|
objective: task.objective,
|
|
52
|
+
title: typeof task.title === 'string' ? task.title : undefined,
|
|
53
|
+
summary: typeof task.summary === 'string' ? task.summary : undefined,
|
|
50
54
|
claims: task.claims,
|
|
51
55
|
budget_tokens: budget?.tokens,
|
|
52
56
|
// D-23/P9-12. Declared here rather than derived by the reader: the board
|
|
53
57
|
// wants a branch link the moment a task is added, which is well before a
|
|
54
58
|
// worktree exists, and worktree.ts's convention is the one that decides
|
|
55
|
-
// what `smith worktree create` will actually cut.
|
|
56
|
-
//
|
|
57
|
-
|
|
59
|
+
// what `smith worktree create` will actually cut. `branchPrefix` is
|
|
60
|
+
// `epicBranchPrefix`'s answer for this epic when `emitTasksAdded` was
|
|
61
|
+
// handed a project dir (a legacy `smith/<epic>/integration` keeps this
|
|
62
|
+
// epic's new branches there too), or the plain `bs` default otherwise —
|
|
63
|
+
// the same choice `worktree create` makes, so the two cannot say
|
|
64
|
+
// different things.
|
|
65
|
+
branch: taskBranchName(epicId, task.task_id, branchPrefix),
|
|
58
66
|
};
|
|
59
67
|
}
|
|
60
68
|
/**
|
|
@@ -108,6 +116,48 @@ async function addedPlanVersions(ctx, opts) {
|
|
|
108
116
|
}
|
|
109
117
|
return highest;
|
|
110
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* The plan record `plan amend` carries for a task only the log added, when it
|
|
121
|
+
* supersedes that task: a dead (`superseded`) record under the task's own id,
|
|
122
|
+
* so the next plan version can say what the log alone could not and
|
|
123
|
+
* `plan ingest` has a record to emit `task-superseded` from.
|
|
124
|
+
*
|
|
125
|
+
* Built from the `task-added` payload where it speaks (objective, claims,
|
|
126
|
+
* case, origin, title, summary, `budget_tokens`) and from honest placeholders
|
|
127
|
+
* where the schema demands a field a follow-up never carried
|
|
128
|
+
* (`output_schema_ref`, acceptance criteria, contract clauses, `diff_lines`):
|
|
129
|
+
* the record is never dispatched, only has to validate. `ui_affecting: false`
|
|
130
|
+
* is set for the same reason; the UI-path rule asks every record claiming one
|
|
131
|
+
* to say so, and this one will never change a screen. `claims` is passed
|
|
132
|
+
* through as written, so a payload with unreadable claims fails the plan's own
|
|
133
|
+
* validation and the amendment is refused, rather than a claim being invented.
|
|
134
|
+
*/
|
|
135
|
+
export function supersededStubFromLog(added, epicId, planVersion) {
|
|
136
|
+
const p = added.payload;
|
|
137
|
+
const str = (v) => typeof v === 'string' && v !== '' ? v : undefined;
|
|
138
|
+
const tokens = p.budget_tokens;
|
|
139
|
+
const note = `Added by the event log; superseded by plan v${planVersion}.`;
|
|
140
|
+
return {
|
|
141
|
+
task_id: added.taskId,
|
|
142
|
+
epic_id: epicId,
|
|
143
|
+
plan_version: planVersion,
|
|
144
|
+
objective: str(p.objective) ?? note,
|
|
145
|
+
output_schema_ref: 'result.schema.json',
|
|
146
|
+
acceptance_criteria: [note],
|
|
147
|
+
claims: added.claims,
|
|
148
|
+
budget: {
|
|
149
|
+
tokens: typeof tokens === 'number' && Number.isInteger(tokens) && tokens >= 1 ? tokens : 1,
|
|
150
|
+
diff_lines: 1,
|
|
151
|
+
},
|
|
152
|
+
contract: { functional_clauses: [note], nonfunctional_clauses: [] },
|
|
153
|
+
case: str(p.case) ?? 'chore',
|
|
154
|
+
origin: str(p.origin) ?? 'user',
|
|
155
|
+
task_status: 'superseded',
|
|
156
|
+
ui_affecting: false,
|
|
157
|
+
...(str(p.title) ? { title: str(p.title) } : {}),
|
|
158
|
+
...(str(p.summary) ? { summary: str(p.summary) } : {}),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
111
161
|
/**
|
|
112
162
|
* Read the lineage's log as a register of tasks — every id a `task-added`
|
|
113
163
|
* has introduced, with the claims it was introduced with.
|
|
@@ -144,6 +194,7 @@ ctx, opts = {}) {
|
|
|
144
194
|
taskId: record.task_id,
|
|
145
195
|
claims: record.payload?.claims,
|
|
146
196
|
epicId: typeof epicId === 'string' ? epicId : undefined,
|
|
197
|
+
payload: record.payload ?? {},
|
|
147
198
|
});
|
|
148
199
|
}
|
|
149
200
|
return [...byId.values()];
|
|
@@ -182,23 +233,14 @@ function specsToIngest(plan) {
|
|
|
182
233
|
return specs;
|
|
183
234
|
}
|
|
184
235
|
/**
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
* and the projector needs it as an event to reach the same conclusion).
|
|
189
|
-
*
|
|
190
|
-
* Per id, not per record — see `specsToIngest`. Emitting the dead copies too
|
|
191
|
-
* would say two false things at once: that a task the plan still lists as
|
|
192
|
-
* `todo` was superseded, and that a 5-task backlog is 13 tasks.
|
|
193
|
-
*
|
|
194
|
-
* Returns only the events it actually appended, so a caller can report "5
|
|
195
|
-
* added, 0 already present" truthfully rather than guessing from the plan.
|
|
236
|
+
* The events an ingest of `plan` would still write into this lineage, in the
|
|
237
|
+
* order it writes them — the whole of `emitTasksAdded`'s decision, kept apart
|
|
238
|
+
* from the appending so a reader can ask the question without writing.
|
|
196
239
|
*/
|
|
197
|
-
|
|
240
|
+
async function pendingIngest(plan, ctx, opts) {
|
|
198
241
|
const added = await addedPlanVersions(ctx, opts);
|
|
199
242
|
const superseded = await idsAlreadyEmitted('task-superseded', ctx, opts);
|
|
200
|
-
const
|
|
201
|
-
const written = [];
|
|
243
|
+
const pending = [];
|
|
202
244
|
// The ids the plan marks as superseded-and-replaced: a dead record beside a
|
|
203
245
|
// live one under the same id is how `draftNextVersion` records an amendment.
|
|
204
246
|
const supersededIds = new Set(plan.tasks.filter((r) => r.task_status === 'superseded').map((r) => r.task_id));
|
|
@@ -216,19 +258,103 @@ export async function emitTasksAdded(plan, ctx, opts = {}) {
|
|
|
216
258
|
// the number would re-add tasks whose spec never changed. The amendment's
|
|
217
259
|
// own marker is what counts — an id the amendment touched carries a dead
|
|
218
260
|
// record beside its live one, which is exactly what `supersededIds` holds.
|
|
261
|
+
//
|
|
262
|
+
// And only for a task that still HAS a live record. A task every record of
|
|
263
|
+
// which is dead (a log-added follow-up `plan amend` retired, or a renamed-
|
|
264
|
+
// away id) has no spec worth adding again; re-adding it as `superseded`
|
|
265
|
+
// would only repeat the row the log already holds.
|
|
219
266
|
const amended = recorded !== undefined &&
|
|
267
|
+
task.task_status !== 'superseded' &&
|
|
220
268
|
supersededIds.has(taskId) &&
|
|
221
269
|
typeof liveVersion === 'number' &&
|
|
222
270
|
liveVersion > recorded;
|
|
223
|
-
if (recorded === undefined || amended)
|
|
224
|
-
|
|
225
|
-
}
|
|
271
|
+
if (recorded === undefined || amended)
|
|
272
|
+
pending.push({ task, eventType: 'task-added' });
|
|
226
273
|
if (task.task_status === 'superseded' && !superseded.has(taskId)) {
|
|
227
|
-
|
|
274
|
+
pending.push({ task, eventType: 'task-superseded' });
|
|
228
275
|
}
|
|
229
276
|
}
|
|
277
|
+
return pending;
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* What `plan ingest` would still write for `plan` into this lineage — empty
|
|
281
|
+
* exactly when the plan in hand has been ingested. Read-only.
|
|
282
|
+
*
|
|
283
|
+
* #250. `wave check --session` admits against the log's task rows, and a plan
|
|
284
|
+
* version that was never ingested has rows the log does not know about, or
|
|
285
|
+
* rows at an older version, so a wave admitted from it is admitted against a
|
|
286
|
+
* backlog nobody recorded. Asked through the same decision `emitTasksAdded`
|
|
287
|
+
* makes, so "ingested" cannot mean one thing here and another at the ingest.
|
|
288
|
+
*/
|
|
289
|
+
export async function planIngestGaps(plan, ctx, opts = {}) {
|
|
290
|
+
const pending = await pendingIngest(plan, ctx, opts);
|
|
291
|
+
return pending.map(({ task, eventType }) => ({ taskId: task.task_id, eventType }));
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Write the plan's backlog into the log: one `task-added` per task id the
|
|
295
|
+
* session has not already recorded, plus a `task-superseded` for any id the
|
|
296
|
+
* plan has no live spec left for (a v(n+1) cut records that as plan state,
|
|
297
|
+
* and the projector needs it as an event to reach the same conclusion).
|
|
298
|
+
*
|
|
299
|
+
* Per id, not per record — see `specsToIngest`. Emitting the dead copies too
|
|
300
|
+
* would say two false things at once: that a task the plan still lists as
|
|
301
|
+
* `todo` was superseded, and that a 5-task backlog is 13 tasks.
|
|
302
|
+
*
|
|
303
|
+
* Returns only the events it actually appended, so a caller can report "5
|
|
304
|
+
* added, 0 already present" truthfully rather than guessing from the plan.
|
|
305
|
+
*
|
|
306
|
+
* `projectDir` is optional: the CLI's `--project` flag. Given, the branch
|
|
307
|
+
* prefix each `task-added` declares is resolved once per epic via
|
|
308
|
+
* `epicBranchPrefix` — a legacy `smith/<epic>/integration` epic keeps landing
|
|
309
|
+
* `smith/...` here, matching what `worktree create` will actually cut.
|
|
310
|
+
* Absent, every task gets the plain `bs` default — correct for a brand-new
|
|
311
|
+
* epic, which has no worktree convention to ask git about yet.
|
|
312
|
+
*/
|
|
313
|
+
export async function emitTasksAdded(plan, ctx, opts = {}, projectDir) {
|
|
314
|
+
const scoped = planScoped(ctx, plan);
|
|
315
|
+
const written = [];
|
|
316
|
+
const branchPrefixes = new Map();
|
|
317
|
+
for (const { task, eventType } of await pendingIngest(plan, ctx, opts)) {
|
|
318
|
+
let payload;
|
|
319
|
+
if (eventType === 'task-added') {
|
|
320
|
+
const epicId = typeof task.epic_id === 'string' ? task.epic_id : plan.epic_id;
|
|
321
|
+
let prefix = branchPrefixes.get(epicId);
|
|
322
|
+
if (prefix === undefined) {
|
|
323
|
+
prefix = projectDir ? epicBranchPrefix(projectDir, epicId) : BRANCH_PREFIX;
|
|
324
|
+
branchPrefixes.set(epicId, prefix);
|
|
325
|
+
}
|
|
326
|
+
payload = addedPayload(plan, task, prefix);
|
|
327
|
+
}
|
|
328
|
+
else {
|
|
329
|
+
payload = { epic_id: plan.epic_id };
|
|
330
|
+
}
|
|
331
|
+
written.push(await appendEvent(envelope(scoped, eventType, payload, task.task_id), opts));
|
|
332
|
+
}
|
|
230
333
|
return written;
|
|
231
334
|
}
|
|
335
|
+
/**
|
|
336
|
+
* The ids in `taskIds` a wave may not admit, read against the lineage's task
|
|
337
|
+
* rows: `missing` has no row at all, `terminal` has one whose `task_status`
|
|
338
|
+
* is closed to further work (CLOSED_TO_FURTHER_WORK: completed, superseded,
|
|
339
|
+
* waived). A `failed` or `escalated` task is terminal but still held by an
|
|
340
|
+
* operator, and the escalation ladder re-admits it, so it passes. Pure; the
|
|
341
|
+
* caller folds the rows and canonicalizes the ids.
|
|
342
|
+
*
|
|
343
|
+
* #250. A superseded id admitted by `wave check --session` went on to be
|
|
344
|
+
* dispatched, gated and merged as if it were live work.
|
|
345
|
+
*/
|
|
346
|
+
export function unadmissibleTasks(taskIds, statusOf) {
|
|
347
|
+
const missing = [];
|
|
348
|
+
const terminal = [];
|
|
349
|
+
for (const taskId of taskIds) {
|
|
350
|
+
const taskStatus = statusOf.get(taskId);
|
|
351
|
+
if (taskStatus === undefined)
|
|
352
|
+
missing.push(taskId);
|
|
353
|
+
else if (CLOSED_TO_FURTHER_WORK.has(taskStatus))
|
|
354
|
+
terminal.push({ taskId, taskStatus });
|
|
355
|
+
}
|
|
356
|
+
return { missing, terminal };
|
|
357
|
+
}
|
|
232
358
|
/** One arrow's identity in the log: who depends on whom, and in what sense. */
|
|
233
359
|
function edgeKey(task, dependsOn, edgeType) {
|
|
234
360
|
return `${task}\u0000${dependsOn}\u0000${edgeType}`;
|
|
@@ -366,6 +492,24 @@ export async function emitTaskBlocked(taskId, error, ctx, opts = {}) {
|
|
|
366
492
|
export function followUpTaskId(epicId, fingerprint) {
|
|
367
493
|
return `${epicId}/followup-${fingerprint.slice(0, 8)}`;
|
|
368
494
|
}
|
|
495
|
+
/** The one project the lineage stamped on `fromTaskId`'s events, or undefined. */
|
|
496
|
+
function fromTaskProject(lineage, fromTaskId) {
|
|
497
|
+
if (fromTaskId === undefined)
|
|
498
|
+
return undefined;
|
|
499
|
+
const fromEpic = epicOfTaskId(fromTaskId);
|
|
500
|
+
const stamped = new Set();
|
|
501
|
+
for (const { record } of lineage) {
|
|
502
|
+
if (!record.project || !record.task_id || !taskIdsMatch(record.task_id, fromTaskId))
|
|
503
|
+
continue;
|
|
504
|
+
// A bare id does not say which epic it is in; a record that names another
|
|
505
|
+
// epic (task events carry `payload.epic_id`) belongs to another task.
|
|
506
|
+
const named = record.payload?.epic_id;
|
|
507
|
+
if (fromEpic !== null && typeof named === 'string' && named !== fromEpic)
|
|
508
|
+
continue;
|
|
509
|
+
stamped.add(record.project);
|
|
510
|
+
}
|
|
511
|
+
return stamped.size === 1 ? [...stamped][0] : undefined;
|
|
512
|
+
}
|
|
369
513
|
/**
|
|
370
514
|
* Record a follow-up task for a finding no open task can own: the file's
|
|
371
515
|
* owner already merged, two tasks claim it equally, or nobody claims it at
|
|
@@ -383,10 +527,18 @@ export function followUpTaskId(epicId, fingerprint) {
|
|
|
383
527
|
* (epic.ts's clearedFollowUpFindings).
|
|
384
528
|
*/
|
|
385
529
|
export async function emitFollowUpTask(input, ctx, opts = {}) {
|
|
386
|
-
const
|
|
387
|
-
if (added.
|
|
530
|
+
const lineage = await readLineageEvents(ctx.sessionId, opts);
|
|
531
|
+
if (lineage.some(({ record }) => record.event_type === 'task-added' && record.task_id === input.taskId)) {
|
|
388
532
|
return null;
|
|
389
|
-
|
|
533
|
+
}
|
|
534
|
+
// A follow-up is minted inside a wave, where no plan is at hand and the
|
|
535
|
+
// wave-runner stamps nothing. So the caller's project wins (a caller holding
|
|
536
|
+
// the plan passes its project here, planScoped's fallback), then the one
|
|
537
|
+
// the log already holds for the task the finding came from. Two different
|
|
538
|
+
// stamps on that task settle nothing: the envelope stays absent rather
|
|
539
|
+
// than picking one.
|
|
540
|
+
const project = ctx.project ?? fromTaskProject(lineage, input.fromTaskId);
|
|
541
|
+
return appendEvent(envelope(project === undefined ? ctx : { ...ctx, project }, 'task-added', {
|
|
390
542
|
epic_id: input.epicId,
|
|
391
543
|
case: 'bugfix',
|
|
392
544
|
origin: 'escalation',
|
|
@@ -72,3 +72,17 @@ export const TERMINAL_OK_TASK_STATUSES = new Set(['completed', 'waived']);
|
|
|
72
72
|
* parking silently on a task nobody is working.
|
|
73
73
|
*/
|
|
74
74
|
export const HELD_OPEN_BY_AN_OPERATOR = new Set(['failed', 'escalated']);
|
|
75
|
+
/**
|
|
76
|
+
* Task statuses that mean "this task's diff is closed to further work": the
|
|
77
|
+
* branch already landed (`completed`), the team already accepted the gap
|
|
78
|
+
* (`waived`), or a later plan version replaced the task outright
|
|
79
|
+
* (`superseded`).
|
|
80
|
+
*
|
|
81
|
+
* That is every terminal status except the ones an operator is still holding,
|
|
82
|
+
* subtracted here so the remainder is derived from the two rosters above
|
|
83
|
+
* rather than typed out as a third. Read by claims.ts (a finding about a
|
|
84
|
+
* closed task's files mints a follow-up) and by `wave check --session` (a
|
|
85
|
+
* closed task is refused admission; a held one is re-admitted by the
|
|
86
|
+
* escalation ladder).
|
|
87
|
+
*/
|
|
88
|
+
export const CLOSED_TO_FURTHER_WORK = new Set([...TERMINAL_TASK_STATUSES].filter((status) => !HELD_OPEN_BY_AN_OPERATOR.has(status)));
|
|
@@ -8,18 +8,37 @@ import { taskIdsMatch } from './taskId.js';
|
|
|
8
8
|
*/
|
|
9
9
|
const TEST_GATE_EVENT = 'testgate-result';
|
|
10
10
|
/**
|
|
11
|
-
* How a dispatched agent's turn is recorded as over, and which payload
|
|
12
|
-
*
|
|
13
|
-
* nicety: the older half of the log names the role
|
|
14
|
-
* and reading only the payload would report those
|
|
15
|
-
* reported — an `unverifiable` about evidence the
|
|
11
|
+
* How a dispatched agent's turn is recorded as over, and which payload
|
|
12
|
+
* field(s) name the role that ended it, tried in order. Falling back to the
|
|
13
|
+
* record's `actor` is not a nicety: the older half of the log names the role
|
|
14
|
+
* there and nowhere else, and reading only the payload would report those
|
|
15
|
+
* runs as testers that never reported — an `unverifiable` about evidence the
|
|
16
|
+
* log is holding.
|
|
17
|
+
*
|
|
18
|
+
* `error-logged` tries `agent` before `agent_role`: this reader's own
|
|
19
|
+
* historical key first, `agent_role` (the dispatch's) as the fallback for a
|
|
20
|
+
* hand-written event that only carries that one — the same either-key
|
|
21
|
+
* reading delegation.ts and agents-registry.ts already give this event type.
|
|
22
|
+
* Each of the three readers keeps its own historical key first rather than
|
|
23
|
+
* all agreeing on one order, since `agent` and `agent_role` were each the
|
|
24
|
+
* only key some reader ever had for this event type before the other was
|
|
25
|
+
* added as a fallback.
|
|
16
26
|
*/
|
|
17
27
|
const TERMINAL_ROLE_EVENTS = {
|
|
18
|
-
'task-result-recorded': 'agent',
|
|
19
|
-
'error-logged': 'agent',
|
|
20
|
-
'judge-reported': 'agent_role',
|
|
21
|
-
'judge-verdict': 'agent',
|
|
28
|
+
'task-result-recorded': ['agent'],
|
|
29
|
+
'error-logged': ['agent', 'agent_role'],
|
|
30
|
+
'judge-reported': ['agent_role'],
|
|
31
|
+
'judge-verdict': ['agent'],
|
|
22
32
|
};
|
|
33
|
+
/** The first of `keys` that names a non-empty string in `payload`, or null. */
|
|
34
|
+
function firstPayloadString(payload, keys) {
|
|
35
|
+
for (const key of keys) {
|
|
36
|
+
const value = payloadString(payload, key);
|
|
37
|
+
if (value !== null)
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
23
42
|
function payloadString(payload, key) {
|
|
24
43
|
const value = payload[key];
|
|
25
44
|
return typeof value === 'string' && value.length > 0 ? value : null;
|
|
@@ -46,11 +65,11 @@ export function readTerminalRoleRecords(events) {
|
|
|
46
65
|
const records = [];
|
|
47
66
|
for (const stored of events) {
|
|
48
67
|
const record = stored.record;
|
|
49
|
-
const
|
|
50
|
-
if (!
|
|
68
|
+
const fields = TERMINAL_ROLE_EVENTS[record.event_type];
|
|
69
|
+
if (!fields)
|
|
51
70
|
continue;
|
|
52
71
|
const payload = (record.payload ?? {});
|
|
53
|
-
const role =
|
|
72
|
+
const role = firstPayloadString(payload, fields) ?? record.actor;
|
|
54
73
|
if (!role)
|
|
55
74
|
continue;
|
|
56
75
|
records.push({
|