@ikon85/agent-workflow-kit 0.38.0 → 0.39.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/.agents/skills/kit-update/SKILL.md +33 -1
- package/.agents/skills/setup-workflow/SKILL.md +34 -3
- package/.agents/skills/setup-workflow/board-sync.md +6 -2
- package/.agents/skills/setup-workflow/workflow-advisories.md +34 -0
- package/.agents/skills/setup-workflow/worktree-lifecycle.md +31 -2
- package/.agents/skills/wrapup/SKILL.md +24 -0
- package/.claude/hooks/drift-guard.py +212 -21
- package/.claude/skills/kit-update/SKILL.md +33 -1
- package/.claude/skills/setup-workflow/SKILL.md +34 -3
- package/.claude/skills/setup-workflow/board-sync.md +6 -2
- package/.claude/skills/setup-workflow/workflow-advisories.md +34 -0
- package/.claude/skills/setup-workflow/worktree-lifecycle.md +31 -2
- package/.claude/skills/wrapup/SKILL.md +24 -0
- package/README.md +37 -0
- package/agent-workflow-kit.package.json +35 -19
- package/package.json +1 -1
- package/scripts/board_bootstrap.py +367 -0
- package/scripts/kit-update-pr.mjs +20 -7
- package/scripts/kit-update-pr.test.mjs +29 -0
- package/scripts/profile_globs.py +347 -0
- package/scripts/test_board_bootstrap.py +348 -0
- package/scripts/test_drift_guard_diagnostics.py +295 -0
- package/scripts/test_profile_globs.py +280 -0
- package/scripts/test_skill_setup_workflow_seeds.py +87 -0
- package/scripts/test_worktree_wrapup_contract.py +588 -0
- package/scripts/workflow-advisories/core.py +29 -4
- package/scripts/worktree-lifecycle/README.md +35 -4
- package/scripts/worktree-lifecycle/core.py +211 -60
- package/scripts/wrapup-land.py +179 -34
- package/src/cli.mjs +32 -1
- package/src/commands/update.mjs +30 -21
- package/src/consumer-migrations.json +19 -0
- package/src/lib/bundle.mjs +10 -0
- package/src/lib/consumerMigrations.mjs +161 -0
package/scripts/wrapup-land.py
CHANGED
|
@@ -71,6 +71,14 @@ INFRA_FAILURE_RE = re.compile(
|
|
|
71
71
|
)
|
|
72
72
|
|
|
73
73
|
|
|
74
|
+
ABANDON_ATTEMPT_FLAG = "--abandon-unfinished-attempt"
|
|
75
|
+
RECOVER_CANONICAL_CLEANUP_FLAG = "--recover-canonical-cleanup"
|
|
76
|
+
CANONICAL_DRIFT_RECOVERY_HINT = (
|
|
77
|
+
f"rerun land with {RECOVER_CANONICAL_CLEANUP_FLAG} to revalidate the frozen "
|
|
78
|
+
"landing evidence against canonical policy and resume teardown"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
74
82
|
class Stop(Exception):
|
|
75
83
|
def __init__(self, step: str, reason: str, detail: str = ""):
|
|
76
84
|
super().__init__(reason)
|
|
@@ -461,9 +469,8 @@ def load_candidate_landing_profile(core, wt: str):
|
|
|
461
469
|
return candidate
|
|
462
470
|
|
|
463
471
|
|
|
464
|
-
def
|
|
465
|
-
"""
|
|
466
|
-
candidate = load_candidate_landing_profile(core, wt)
|
|
472
|
+
def load_merged_canonical_profile(core, main_tree: str):
|
|
473
|
+
"""Read the merged canonical cleanup policy from origin/main."""
|
|
467
474
|
result = core.run(
|
|
468
475
|
[
|
|
469
476
|
"git", "show",
|
|
@@ -482,6 +489,13 @@ def load_canonical_landing_profile(core, wt: str, main_tree: str):
|
|
|
482
489
|
raise core.LifecycleError(
|
|
483
490
|
"merged canonical landing artifact policy is not configured"
|
|
484
491
|
)
|
|
492
|
+
return canonical
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def load_canonical_landing_profile(core, wt: str, main_tree: str):
|
|
496
|
+
"""Authorize deletion only after the candidate policy is canonical."""
|
|
497
|
+
candidate = load_candidate_landing_profile(core, wt)
|
|
498
|
+
canonical = load_merged_canonical_profile(core, main_tree)
|
|
485
499
|
if (
|
|
486
500
|
candidate.landing_generated_artifact_patterns
|
|
487
501
|
!= canonical.landing_generated_artifact_patterns
|
|
@@ -489,11 +503,9 @@ def load_canonical_landing_profile(core, wt: str, main_tree: str):
|
|
|
489
503
|
):
|
|
490
504
|
raise core.LifecycleError(
|
|
491
505
|
"worktree cleanup policy differs from merged canonical origin/main"
|
|
506
|
+
f"; {CANONICAL_DRIFT_RECOVERY_HINT}"
|
|
492
507
|
)
|
|
493
|
-
|
|
494
|
-
core.LANDING_ATTEMPT_FILE
|
|
495
|
-
)
|
|
496
|
-
if not os.path.lexists(attempt_path):
|
|
508
|
+
if not core.landing_attempt_exists(core.landing_attempt_path(Path(wt))):
|
|
497
509
|
raise core.LifecycleError(
|
|
498
510
|
"landing attempt is missing before canonical cleanup authorization"
|
|
499
511
|
)
|
|
@@ -504,17 +516,22 @@ def load_canonical_landing_profile(core, wt: str, main_tree: str):
|
|
|
504
516
|
):
|
|
505
517
|
raise core.LifecycleError(
|
|
506
518
|
"landing attempt cleanup policy differs from merged canonical origin/main"
|
|
519
|
+
f"; {CANONICAL_DRIFT_RECOVERY_HINT}"
|
|
507
520
|
)
|
|
508
521
|
return canonical
|
|
509
522
|
|
|
510
523
|
|
|
511
|
-
def
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
524
|
+
def load_canonical_recovery_profile(core, wt: str, main_tree: str):
|
|
525
|
+
"""Authorize post-merge cleanup from canonical policy alone after drift.
|
|
526
|
+
|
|
527
|
+
The committed worktree candidate is deliberately never consulted here: after
|
|
528
|
+
canonical drift it is stale, and trusting it could only widen what may be
|
|
529
|
+
deleted. Every authority boundary re-validates the frozen evidence against
|
|
530
|
+
the merged canonical policy instead.
|
|
531
|
+
"""
|
|
532
|
+
canonical = load_merged_canonical_profile(core, main_tree)
|
|
533
|
+
core.canonical_recovery_evidence(canonical, Path(wt))
|
|
534
|
+
return canonical
|
|
518
535
|
|
|
519
536
|
|
|
520
537
|
def landing_start_artifact_inventory(wt: str, main_tree: str) -> dict:
|
|
@@ -522,6 +539,10 @@ def landing_start_artifact_inventory(wt: str, main_tree: str) -> dict:
|
|
|
522
539
|
try:
|
|
523
540
|
profile = load_candidate_landing_profile(core, wt)
|
|
524
541
|
return core.landing_start_artifact_inventory(profile, Path(wt))
|
|
542
|
+
except core.LegacyLandingAttempt as error:
|
|
543
|
+
# Legacy, not damage: report the classification and the safe route out
|
|
544
|
+
# verbatim, without the corruption framing of a guard failure.
|
|
545
|
+
raise Stop("cleanup", str(error)) from error
|
|
525
546
|
except core.LifecycleError as error:
|
|
526
547
|
raise Stop("cleanup", f"shared cleanup guard failed: {error}") from error
|
|
527
548
|
|
|
@@ -581,6 +602,16 @@ def abandon_unfinished_landing_attempt(wt: str, main_tree: str) -> str:
|
|
|
581
602
|
raise Stop("cleanup", f"shared cleanup guard failed: {error}") from error
|
|
582
603
|
|
|
583
604
|
|
|
605
|
+
def canonical_recovery_evidence(wt: str, main_tree: str) -> tuple[dict, ...]:
|
|
606
|
+
"""Revalidate the frozen landing evidence against canonical policy alone."""
|
|
607
|
+
core = load_worktree_cleanup_core()
|
|
608
|
+
try:
|
|
609
|
+
canonical = load_merged_canonical_profile(core, main_tree)
|
|
610
|
+
return core.canonical_recovery_evidence(canonical, Path(wt))
|
|
611
|
+
except core.LifecycleError as error:
|
|
612
|
+
raise Stop("cleanup", f"shared cleanup guard failed: {error}") from error
|
|
613
|
+
|
|
614
|
+
|
|
584
615
|
def landing_verified_scratch_files(
|
|
585
616
|
wt: str,
|
|
586
617
|
main_tree: str,
|
|
@@ -605,10 +636,13 @@ def ensure_worktree_removable(
|
|
|
605
636
|
*,
|
|
606
637
|
verified_scratch_files: tuple[str, ...] = (),
|
|
607
638
|
verified_scratch_evidence: tuple[dict, ...] = (),
|
|
639
|
+
profile_loader=None,
|
|
608
640
|
):
|
|
609
641
|
core = load_worktree_cleanup_core()
|
|
642
|
+
# Resolved at call time so the module attribute stays the single seam.
|
|
643
|
+
loader = profile_loader or load_canonical_landing_profile
|
|
610
644
|
try:
|
|
611
|
-
profile =
|
|
645
|
+
profile = loader(core, wt, main_tree)
|
|
612
646
|
kwargs = {"merge_target": "origin/main"}
|
|
613
647
|
paths = (
|
|
614
648
|
tuple(item["path"] for item in verified_scratch_evidence)
|
|
@@ -639,11 +673,13 @@ def remove_verified_worktree_scratch(
|
|
|
639
673
|
*,
|
|
640
674
|
verified_scratch_files: tuple[str, ...] = (),
|
|
641
675
|
verified_scratch_evidence: tuple[dict, ...] = (),
|
|
676
|
+
profile_loader=None,
|
|
642
677
|
):
|
|
643
678
|
"""Re-verify and delete only assessed regular scratch files."""
|
|
644
679
|
core = load_worktree_cleanup_core()
|
|
680
|
+
loader = profile_loader or load_canonical_landing_profile
|
|
645
681
|
try:
|
|
646
|
-
profile =
|
|
682
|
+
profile = loader(core, wt, main_tree)
|
|
647
683
|
evidence = verified_scratch_evidence
|
|
648
684
|
paths = tuple(item["path"] for item in evidence) or verified_scratch_files
|
|
649
685
|
latest = core.cleanup_assessment(
|
|
@@ -920,6 +956,105 @@ def cmd_commit(args) -> dict:
|
|
|
920
956
|
return {"committed": True, "sha": sha, "allowed_matches": bool(hits)}
|
|
921
957
|
|
|
922
958
|
|
|
959
|
+
def retire_local_branch(branch: str, main_tree: str, report: dict) -> None:
|
|
960
|
+
"""Fast-forward main, then retire the merged local branch with `-d` only."""
|
|
961
|
+
git(["fetch", "origin", "--prune"], cwd=main_tree)
|
|
962
|
+
git(["checkout", "main"], cwd=main_tree)
|
|
963
|
+
p = git(["pull", "--ff-only"], cwd=main_tree)
|
|
964
|
+
if p.returncode != 0:
|
|
965
|
+
raise Stop("5 main-ff", "no fast-forward possible — diverged main is an anomaly",
|
|
966
|
+
(p.stderr or p.stdout).strip()[-1000:])
|
|
967
|
+
if git(["show-ref", "--verify", "--quiet", f"refs/heads/{branch}"],
|
|
968
|
+
cwd=main_tree).returncode != 0:
|
|
969
|
+
report["branch_retired"] = "already absent"
|
|
970
|
+
return
|
|
971
|
+
if branch in worktree_map(main_tree)[1]:
|
|
972
|
+
report["branch_retired"] = "refused: still checked out"
|
|
973
|
+
report["warnings"].append(
|
|
974
|
+
f"branch -d {branch} refused (still checked out?) — never -D"
|
|
975
|
+
)
|
|
976
|
+
return
|
|
977
|
+
p = git(["branch", "-d", branch], cwd=main_tree)
|
|
978
|
+
report["branch_retired"] = p.returncode == 0
|
|
979
|
+
if p.returncode != 0:
|
|
980
|
+
report["warnings"].append(
|
|
981
|
+
f"branch -d {branch} refused: {(p.stderr or '').strip()[:200]}"
|
|
982
|
+
)
|
|
983
|
+
|
|
984
|
+
|
|
985
|
+
def recover_canonical_cleanup(
|
|
986
|
+
branch: str,
|
|
987
|
+
wt: str | None,
|
|
988
|
+
wt_exists: bool,
|
|
989
|
+
main_tree: str,
|
|
990
|
+
) -> dict:
|
|
991
|
+
"""Resume post-merge teardown after canonical cleanup-policy drift.
|
|
992
|
+
|
|
993
|
+
Canonical drift keeps the normal path fail-closed: the attempt is bound to
|
|
994
|
+
the policy that nominated its evidence, and that binding is never relaxed.
|
|
995
|
+
This route is the supported way out, not a bypass — it re-reads the merged
|
|
996
|
+
canonical policy, requires the branch to already be an ancestor of canonical
|
|
997
|
+
`origin/main`, and revalidates every frozen identity against that canonical
|
|
998
|
+
policy before the same shared assessment and removal primitives run. Nothing
|
|
999
|
+
outside the revalidated evidence can be deleted, and rerunning it after a
|
|
1000
|
+
successful teardown is a no-op.
|
|
1001
|
+
"""
|
|
1002
|
+
report: dict = {
|
|
1003
|
+
"recovery": "canonical-cleanup",
|
|
1004
|
+
"branch": branch,
|
|
1005
|
+
"warnings": [],
|
|
1006
|
+
}
|
|
1007
|
+
git(["fetch", "origin", "main"], cwd=main_tree, check=True)
|
|
1008
|
+
if not wt_exists:
|
|
1009
|
+
report["worktree_removed"] = None
|
|
1010
|
+
report["cleanup_guard"] = None
|
|
1011
|
+
retire_local_branch(branch, main_tree, report)
|
|
1012
|
+
report["main_sha"] = git(["log", "--oneline", "-1"], cwd=main_tree,
|
|
1013
|
+
check=True).stdout.strip()
|
|
1014
|
+
return report
|
|
1015
|
+
assert wt is not None
|
|
1016
|
+
if git(["status", "--porcelain"], cwd=wt, check=True).stdout.strip():
|
|
1017
|
+
raise Stop("recover-cleanup", "worktree dirty — run `commit` first", wt)
|
|
1018
|
+
if git(["merge-base", "--is-ancestor", branch, "origin/main"],
|
|
1019
|
+
cwd=main_tree).returncode != 0:
|
|
1020
|
+
raise Stop(
|
|
1021
|
+
"recover-cleanup",
|
|
1022
|
+
"branch is not merged into canonical origin/main — canonical cleanup "
|
|
1023
|
+
"recovery only resumes an already-merged teardown",
|
|
1024
|
+
branch,
|
|
1025
|
+
)
|
|
1026
|
+
evidence = canonical_recovery_evidence(wt, main_tree)
|
|
1027
|
+
assessment = ensure_worktree_removable(
|
|
1028
|
+
wt,
|
|
1029
|
+
main_tree,
|
|
1030
|
+
verified_scratch_evidence=evidence,
|
|
1031
|
+
profile_loader=load_canonical_recovery_profile,
|
|
1032
|
+
)
|
|
1033
|
+
report["cleanup_guard"] = {
|
|
1034
|
+
"assumptions_read": bool(assessment.assumptions),
|
|
1035
|
+
"landing_generated_files": [item["path"] for item in evidence],
|
|
1036
|
+
}
|
|
1037
|
+
report["killed_processes"] = kill_worktree_processes(wt)
|
|
1038
|
+
remove_verified_worktree_scratch(
|
|
1039
|
+
wt,
|
|
1040
|
+
main_tree,
|
|
1041
|
+
assessment,
|
|
1042
|
+
verified_scratch_evidence=evidence,
|
|
1043
|
+
profile_loader=load_canonical_recovery_profile,
|
|
1044
|
+
)
|
|
1045
|
+
p = git(["worktree", "remove", wt], cwd=main_tree)
|
|
1046
|
+
if p.returncode != 0:
|
|
1047
|
+
raise Stop("recover-cleanup", "git worktree remove refused — no --force; "
|
|
1048
|
+
"check for surviving processes (lsof/pgrep)",
|
|
1049
|
+
(p.stderr or p.stdout).strip()[-1000:])
|
|
1050
|
+
git(["worktree", "prune"], cwd=main_tree)
|
|
1051
|
+
report["worktree_removed"] = wt
|
|
1052
|
+
retire_local_branch(branch, main_tree, report)
|
|
1053
|
+
report["main_sha"] = git(["log", "--oneline", "-1"], cwd=main_tree,
|
|
1054
|
+
check=True).stdout.strip()
|
|
1055
|
+
return report
|
|
1056
|
+
|
|
1057
|
+
|
|
923
1058
|
def cmd_land(args) -> dict:
|
|
924
1059
|
report: dict = {"stops": [], "warnings": []}
|
|
925
1060
|
main_tree, branches = worktree_map()
|
|
@@ -931,13 +1066,13 @@ def cmd_land(args) -> dict:
|
|
|
931
1066
|
branch = args.branch
|
|
932
1067
|
wt = branches.get(branch)
|
|
933
1068
|
wt_exists = wt is not None and Path(wt).is_dir()
|
|
1069
|
+
if getattr(args, "recover_canonical_cleanup", False):
|
|
1070
|
+
return recover_canonical_cleanup(branch, wt, wt_exists, main_tree)
|
|
934
1071
|
profile = load_profile()
|
|
935
1072
|
default_section = profile.get("headings", {}).get("vorBau", "Vor Bau zu klären")
|
|
936
1073
|
|
|
937
1074
|
# drift markers from the build-time log — mechanical, no gate
|
|
938
1075
|
markers: list[dict] = []
|
|
939
|
-
artifact_baseline_digest: str | None = None
|
|
940
|
-
landing_start_files: tuple[str, ...] = ()
|
|
941
1076
|
landing_attempt: dict | None = None
|
|
942
1077
|
generated_evidence: tuple[dict, ...] = ()
|
|
943
1078
|
if wt_exists:
|
|
@@ -959,16 +1094,18 @@ def cmd_land(args) -> dict:
|
|
|
959
1094
|
raise Stop(
|
|
960
1095
|
"cleanup",
|
|
961
1096
|
"unfinished landing generator attempt has no frozen output evidence; "
|
|
962
|
-
"classify its files, then rerun with
|
|
1097
|
+
f"classify its files, then rerun with {ABANDON_ATTEMPT_FLAG}",
|
|
963
1098
|
)
|
|
964
|
-
|
|
965
|
-
|
|
1099
|
+
# Retained structural backstop: a journal on the active contract always
|
|
1100
|
+
# carries an empty `generatedFiles`, because a landing-start blocker is
|
|
1101
|
+
# refused before the journal is written. A non-empty list therefore means
|
|
1102
|
+
# tampered or migrated evidence — never a path this run may claim.
|
|
1103
|
+
if landing_attempt["generatedFiles"]:
|
|
966
1104
|
raise Stop(
|
|
967
1105
|
"cleanup",
|
|
968
1106
|
"landing-start generated paths are consumer-owned and protected: "
|
|
969
|
-
+ ", ".join(
|
|
1107
|
+
+ ", ".join(landing_attempt["generatedFiles"]),
|
|
970
1108
|
)
|
|
971
|
-
artifact_baseline_digest = landing_attempt["baselineDigest"]
|
|
972
1109
|
annahmen = Path(wt) / "ANNAHMEN.md"
|
|
973
1110
|
if annahmen.is_file():
|
|
974
1111
|
markers, malformed = parse_annahmen(annahmen.read_text(), default_section)
|
|
@@ -1077,18 +1214,16 @@ def cmd_land(args) -> dict:
|
|
|
1077
1214
|
verified_scratch_evidence=generated_evidence,
|
|
1078
1215
|
)
|
|
1079
1216
|
report["cleanup_guard"] = {
|
|
1080
|
-
"
|
|
1081
|
-
"assumptions_read": bool(cleanup and cleanup.assumptions),
|
|
1217
|
+
"assumptions_read": bool(cleanup.assumptions),
|
|
1082
1218
|
"landing_generated_files": list(generated),
|
|
1083
1219
|
}
|
|
1084
1220
|
report["killed_processes"] = kill_worktree_processes(wt)
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
)
|
|
1221
|
+
remove_verified_worktree_scratch(
|
|
1222
|
+
wt,
|
|
1223
|
+
main_tree,
|
|
1224
|
+
cleanup,
|
|
1225
|
+
verified_scratch_evidence=generated_evidence,
|
|
1226
|
+
)
|
|
1092
1227
|
p = git(["worktree", "remove", wt], cwd=main_tree)
|
|
1093
1228
|
if p.returncode != 0:
|
|
1094
1229
|
raise Stop("4 worktree-remove", "git worktree remove refused — no --force; "
|
|
@@ -1216,14 +1351,24 @@ def main() -> int:
|
|
|
1216
1351
|
l.add_argument("--body-file", help="final PR body (create or overwrite)")
|
|
1217
1352
|
l.add_argument("--anchor", help="wave-anchor issue # (derived via parent-of when omitted)")
|
|
1218
1353
|
l.add_argument("--skip-malformed-drift", action="store_true")
|
|
1219
|
-
l.
|
|
1220
|
-
|
|
1354
|
+
recovery = l.add_mutually_exclusive_group()
|
|
1355
|
+
recovery.add_argument(
|
|
1356
|
+
ABANDON_ATTEMPT_FLAG,
|
|
1221
1357
|
action="store_true",
|
|
1222
1358
|
help=(
|
|
1223
1359
|
"archive an interrupted pre-freeze landing attempt without deleting "
|
|
1224
1360
|
"or claiming its ambiguous files"
|
|
1225
1361
|
),
|
|
1226
1362
|
)
|
|
1363
|
+
recovery.add_argument(
|
|
1364
|
+
RECOVER_CANONICAL_CLEANUP_FLAG,
|
|
1365
|
+
action="store_true",
|
|
1366
|
+
help=(
|
|
1367
|
+
"after canonical cleanup-policy drift, revalidate the frozen landing "
|
|
1368
|
+
"evidence against canonical origin/main and resume the merged "
|
|
1369
|
+
"worktree's teardown idempotently"
|
|
1370
|
+
),
|
|
1371
|
+
)
|
|
1227
1372
|
args = ap.parse_args()
|
|
1228
1373
|
|
|
1229
1374
|
try:
|
package/src/cli.mjs
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
} from './lib/contributionRouting.mjs';
|
|
17
17
|
import { CONSUMER_ORIGIN, KIT_ORIGIN } from './lib/manifest.mjs';
|
|
18
18
|
import { nonInteractiveUpdateDecision } from './lib/updateDecisions.mjs';
|
|
19
|
+
import { renderRequiredMigration } from './lib/consumerMigrations.mjs';
|
|
19
20
|
import { currentAgentSurface } from './lib/agentSurfaceRegistry.mjs';
|
|
20
21
|
import { createCommandAdapter } from '../scripts/release-state.mjs';
|
|
21
22
|
import { installedIdentityFromDir } from '../scripts/release-parity.mjs';
|
|
@@ -43,6 +44,8 @@ export async function runCli({
|
|
|
43
44
|
const keepDeleted = args.includes('--keep-deleted');
|
|
44
45
|
const restoreDeleted = args.includes('--restore-deleted');
|
|
45
46
|
const ownershipState = args.find((arg) => arg.startsWith('--as='))?.slice('--as='.length);
|
|
47
|
+
// Machine-readable rendering of the same update record the interactive run prints.
|
|
48
|
+
const jsonReport = cmd === 'update' && args.includes('--json');
|
|
46
49
|
let exitCode = 0;
|
|
47
50
|
|
|
48
51
|
if (cmd === 'update' && keepDeleted && restoreDeleted) {
|
|
@@ -57,7 +60,7 @@ export async function runCli({
|
|
|
57
60
|
return 1;
|
|
58
61
|
}
|
|
59
62
|
|
|
60
|
-
p.intro('agent-workflow-kit');
|
|
63
|
+
if (!jsonReport) p.intro('agent-workflow-kit');
|
|
61
64
|
|
|
62
65
|
try {
|
|
63
66
|
if (cmd === 'init') {
|
|
@@ -94,6 +97,11 @@ export async function runCli({
|
|
|
94
97
|
releaseIdentities,
|
|
95
98
|
routingProfile: routingProfileOptions(yes),
|
|
96
99
|
});
|
|
100
|
+
if (jsonReport) {
|
|
101
|
+
process.stdout.write(`${JSON.stringify(updateDocument(r), null, 2)}\n`);
|
|
102
|
+
if (r.state === 'failed') return 1;
|
|
103
|
+
return r.state === 'conflicted' ? 2 : 0;
|
|
104
|
+
}
|
|
97
105
|
printPlan(r);
|
|
98
106
|
printRoutingProfile(r.routingProfile);
|
|
99
107
|
for (const c of r.conflicts) p.note(c.diff || '(binary/!text)', `conflict (not applied): ${c.path}`);
|
|
@@ -183,12 +191,31 @@ export async function runCli({
|
|
|
183
191
|
p.outro('');
|
|
184
192
|
}
|
|
185
193
|
} catch (err) {
|
|
194
|
+
if (jsonReport) {
|
|
195
|
+
process.stdout.write(`${JSON.stringify({
|
|
196
|
+
schemaVersion: UPDATE_DOCUMENT_SCHEMA_VERSION, state: 'failed', error: err.message,
|
|
197
|
+
}, null, 2)}\n`);
|
|
198
|
+
return 1;
|
|
199
|
+
}
|
|
186
200
|
p.cancel(`Error: ${err.message}`);
|
|
187
201
|
return 1;
|
|
188
202
|
}
|
|
189
203
|
return exitCode;
|
|
190
204
|
}
|
|
191
205
|
|
|
206
|
+
const UPDATE_DOCUMENT_SCHEMA_VERSION = 1;
|
|
207
|
+
|
|
208
|
+
/** The single structured update record; `printPlan` renders the same source. */
|
|
209
|
+
function updateDocument(r) {
|
|
210
|
+
return {
|
|
211
|
+
schemaVersion: UPDATE_DOCUMENT_SCHEMA_VERSION,
|
|
212
|
+
state: r.state,
|
|
213
|
+
status: r.status ?? null,
|
|
214
|
+
report: r.report,
|
|
215
|
+
...(r.error ? { error: renderUpdateFailure(r) } : {}),
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
192
219
|
const invokedPath = process.argv[1] ? realpathSync(process.argv[1]) : '';
|
|
193
220
|
if (invokedPath === fileURLToPath(import.meta.url)) {
|
|
194
221
|
process.exitCode = await runCli();
|
|
@@ -211,6 +238,10 @@ function printPlan(r) {
|
|
|
211
238
|
lines.push(`${label}: ${r.availability[key].join(', ') || 'none'}`);
|
|
212
239
|
}
|
|
213
240
|
}
|
|
241
|
+
for (const action of r.requiredMigrations ?? []) {
|
|
242
|
+
lines.push(renderRequiredMigration(action));
|
|
243
|
+
lines.push(` ${action.consequence} ${action.remediation}`);
|
|
244
|
+
}
|
|
214
245
|
for (const owned of r.ownedDiffs ?? []) {
|
|
215
246
|
lines.push(`${owned.state} ${owned.path}`);
|
|
216
247
|
if (owned.binary) {
|
package/src/commands/update.mjs
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
import {
|
|
15
15
|
inspectRoutingProfile, reconcileRoutingProfile,
|
|
16
16
|
} from '../lib/routingProfile.mjs';
|
|
17
|
+
import { evaluateConsumerMigrations } from '../lib/consumerMigrations.mjs';
|
|
17
18
|
|
|
18
19
|
const RELEASE_NAME = '@ikon85/agent-workflow-kit';
|
|
19
20
|
|
|
@@ -75,7 +76,14 @@ async function updatePackage(options) {
|
|
|
75
76
|
if (!decisions.has(key)) decisions.set(key, await decide(action, path));
|
|
76
77
|
return decisions.get(key);
|
|
77
78
|
};
|
|
79
|
+
// Required consumer migrations are decisions the consumer still owes; `update`
|
|
80
|
+
// never writes the consumer's project layer, so the pre-apply reading is also
|
|
81
|
+
// the post-apply state. Detected once, rendered by every surface.
|
|
82
|
+
const requiredMigrations = await evaluateConsumerMigrations({
|
|
83
|
+
consumerRoot, kitVersion: pkg.kitVersion,
|
|
84
|
+
});
|
|
78
85
|
const preview = await reconcile({ kitRoot, consumerRoot, decide: choosePreview, dryRun: true });
|
|
86
|
+
preview.requiredMigrations = requiredMigrations;
|
|
79
87
|
let previewFailure;
|
|
80
88
|
try {
|
|
81
89
|
Object.assign(preview, await previewReadinessAdoption({
|
|
@@ -92,7 +100,7 @@ async function updatePackage(options) {
|
|
|
92
100
|
failure: { phase: 'staging', consumerState: 'unchanged' },
|
|
93
101
|
};
|
|
94
102
|
}
|
|
95
|
-
if (dryRun) return { ...preview, state: 'preview', history };
|
|
103
|
+
if (dryRun) return { ...preview, state: 'preview', history, report: reportOf(preview) };
|
|
96
104
|
if (preview.migrationConflicts?.length) {
|
|
97
105
|
return terminal(preview, 'conflicted', history, transition);
|
|
98
106
|
}
|
|
@@ -100,6 +108,7 @@ async function updatePackage(options) {
|
|
|
100
108
|
const resolvedPreview = await resolvePreview({
|
|
101
109
|
kitRoot, consumerRoot, preview, decisions, decide, transition,
|
|
102
110
|
});
|
|
111
|
+
resolvedPreview.requiredMigrations = requiredMigrations;
|
|
103
112
|
if (resolvedPreview.collisions.length) {
|
|
104
113
|
return terminal(resolvedPreview, 'conflicted', history, transition);
|
|
105
114
|
}
|
|
@@ -266,28 +275,28 @@ function hasUpstreamDelta(result) {
|
|
|
266
275
|
|
|
267
276
|
async function terminal(result, state, history, transition) {
|
|
268
277
|
await transition(state);
|
|
278
|
+
return { ...result, state, history, report: reportOf(result) };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function reportOf(result) {
|
|
269
282
|
return {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
deleted: result.deleted,
|
|
285
|
-
localModified: result.userModified,
|
|
286
|
-
conflicts: result.conflicts.map(({ path }) => path),
|
|
287
|
-
keptDeleted: result.keptDeleted,
|
|
288
|
-
},
|
|
289
|
-
recommendation: updateRecommendation(result),
|
|
283
|
+
unchanged: result.unchanged.length,
|
|
284
|
+
added: result.added.length,
|
|
285
|
+
updated: result.updated.length,
|
|
286
|
+
deleted: result.deleted.length,
|
|
287
|
+
localModified: result.userModified.length,
|
|
288
|
+
conflicts: result.conflicts.length,
|
|
289
|
+
keptDeleted: result.keptDeleted.length,
|
|
290
|
+
paths: {
|
|
291
|
+
added: result.added,
|
|
292
|
+
updated: result.updated,
|
|
293
|
+
deleted: result.deleted,
|
|
294
|
+
localModified: result.userModified,
|
|
295
|
+
conflicts: result.conflicts.map(({ path }) => path),
|
|
296
|
+
keptDeleted: result.keptDeleted,
|
|
290
297
|
},
|
|
298
|
+
recommendation: updateRecommendation(result),
|
|
299
|
+
requiredMigrations: result.requiredMigrations ?? [],
|
|
291
300
|
};
|
|
292
301
|
}
|
|
293
302
|
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"migrations": [
|
|
4
|
+
{
|
|
5
|
+
"id": "wrapup-landing-artifact-policy",
|
|
6
|
+
"requiredFrom": "0.38.0",
|
|
7
|
+
"title": "Explicit landing-artifact policy",
|
|
8
|
+
"workflow": "setup-workflow",
|
|
9
|
+
"decision": "wrapup.landingGeneratedArtifactPatterns",
|
|
10
|
+
"detect": {
|
|
11
|
+
"type": "json-key",
|
|
12
|
+
"path": "docs/agents/workflow-capabilities.json",
|
|
13
|
+
"key": ["wrapup", "landingGeneratedArtifactPatterns"]
|
|
14
|
+
},
|
|
15
|
+
"consequence": "wrapup-land fails closed before landing until the decision is committed.",
|
|
16
|
+
"remediation": "Run setup-workflow, review the wrapup.landingGeneratedArtifactPatterns decision it writes into docs/agents/workflow-capabilities.json, and commit that profile change. kit-update never chooses cleanup patterns for you."
|
|
17
|
+
}
|
|
18
|
+
]
|
|
19
|
+
}
|
package/src/lib/bundle.mjs
CHANGED
|
@@ -16,6 +16,10 @@ export const HELPER_FILES = [
|
|
|
16
16
|
// every board-specific value from docs/agents/board-sync.md through it, so it
|
|
17
17
|
// MUST ship or they are broken-on-arrival. Library (imported, not run) → 0o644.
|
|
18
18
|
{ path: 'scripts/board_config.py', kind: 'script', mode: 0o644 },
|
|
19
|
+
// Offered board provisioning for /setup-workflow Section D: creates the
|
|
20
|
+
// project + Status/workflow fields, reads the opaque ids back, and writes the
|
|
21
|
+
// filled profile. Imports board_config (above). Invokable CLI → 0o755.
|
|
22
|
+
{ path: 'scripts/board_bootstrap.py', kind: 'script', mode: 0o755 },
|
|
19
23
|
// Pure Slices-table logic imported by board-sync.py for `anchor-sync` —
|
|
20
24
|
// library (imported, not run) → 0o644. MUST ship or board-sync.py ImportErrors.
|
|
21
25
|
{ path: 'scripts/anchor_table.py', kind: 'script', mode: 0o644 },
|
|
@@ -127,6 +131,12 @@ export const HELPER_FILES = [
|
|
|
127
131
|
{ path: 'scripts/memory-lifecycle/setup.mjs', kind: 'script', mode: 0o644 },
|
|
128
132
|
{ path: 'assets/memory-templates/meta_decision_layer_choice.md', kind: 'template', mode: 0o644 },
|
|
129
133
|
{ path: 'assets/memory-templates/meta_memory_lifecycle.md', kind: 'template', mode: 0o644 },
|
|
134
|
+
// The one repository-relative glob dialect. Both capability cores below load
|
|
135
|
+
// it, so a consumer profile pattern selects the same paths for an advisory
|
|
136
|
+
// and for a cleanup decision. Also the reviewable migration check consumers
|
|
137
|
+
// run after setup or update, so a narrowed or widened pattern is named
|
|
138
|
+
// instead of silently changing deletion authority. Library + CLI → 0o755.
|
|
139
|
+
{ path: 'scripts/profile_globs.py', kind: 'script', mode: 0o755 },
|
|
130
140
|
// Profile-driven Worktree Lifecycle foundation. The setup adapter imports
|
|
131
141
|
// core.py, while capabilities.json keeps the historical 8/8 denominator
|
|
132
142
|
// explicit until the remaining hook and cleanup adapters are activated.
|