@codyswann/lisa 4.45.2 → 4.45.4
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/all/copy-overwrite/scripts/lisa-hooks/block-blind-automerge.sh +523 -70
- package/all/copy-overwrite/scripts/lisa-hooks/block-direct-issue-create.sh +160 -2
- package/all/copy-overwrite/scripts/lisa-hooks/parity-safety-net.sh +115 -12
- package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.js +10 -0
- package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
- package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +19 -12
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/dist/opencode/plugin-templates/lisa-block-direct-issue-create.ts +40 -1
- package/dist/opencode/plugin-templates/parity-safety-net.sh +115 -12
- package/package.json +4 -4
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-github-write-issue/SKILL.md +16 -0
- package/plugins/lisa/hooks/block-blind-automerge.agy.sh +5 -4
- package/plugins/lisa/hooks/block-blind-automerge.sh +523 -70
- package/plugins/lisa/hooks/block-direct-issue-create.sh +160 -2
- package/plugins/lisa/hooks/parity-safety-net.sh +115 -12
- package/plugins/lisa/rules/eager/ready-role-filing.md +3 -2
- package/plugins/lisa/rules/reference/ready-role-filing.md +20 -2
- package/plugins/lisa/skills/lisa-github-write-issue/SKILL.md +16 -0
- package/plugins/lisa-agy/hooks/block-blind-automerge.agy.sh +5 -4
- package/plugins/lisa-agy/hooks/block-blind-automerge.sh +523 -70
- package/plugins/lisa-agy/hooks/block-direct-issue-create.sh +160 -2
- package/plugins/lisa-agy/hooks/parity-safety-net.sh +115 -12
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-agy/skills/lisa-github-write-issue/SKILL.md +16 -0
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-agy/plugin.json +1 -1
- package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/hooks/block-blind-automerge.sh +523 -70
- package/plugins/lisa-copilot/hooks/block-direct-issue-create.sh +160 -2
- package/plugins/lisa-copilot/hooks/parity-safety-net.sh +115 -12
- package/plugins/lisa-copilot/rules/eager/ready-role-filing.md +3 -2
- package/plugins/lisa-copilot/rules/reference/ready-role-filing.md +20 -2
- package/plugins/lisa-copilot/skills/lisa-github-write-issue/SKILL.md +16 -0
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/hooks/block-blind-automerge.sh +523 -70
- package/plugins/lisa-cursor/hooks/block-direct-issue-create.sh +160 -2
- package/plugins/lisa-cursor/hooks/parity-safety-net.sh +115 -12
- package/plugins/lisa-cursor/rules/ready-role-filing-reference.mdc +20 -2
- package/plugins/lisa-cursor/rules/ready-role-filing.mdc +3 -2
- package/plugins/lisa-cursor/skills/lisa-github-write-issue/SKILL.md +16 -0
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-agy/plugin.json +1 -1
- package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-agy/plugin.json +1 -1
- package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-agy/plugin.json +1 -1
- package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-agy/plugin.json +1 -1
- package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-agy/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-agy/plugin.json +1 -1
- package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-agy/plugin.json +1 -1
- package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/src/base/hooks/block-blind-automerge.agy.sh +5 -4
- package/plugins/src/base/hooks/block-blind-automerge.sh +523 -70
- package/plugins/src/base/hooks/block-direct-issue-create.sh +160 -2
- package/plugins/src/base/hooks/parity-safety-net.sh +115 -12
- package/plugins/src/base/rules/eager/ready-role-filing.md +3 -2
- package/plugins/src/base/rules/reference/ready-role-filing.md +20 -2
- package/plugins/src/base/skills/lisa-github-write-issue/SKILL.md +16 -0
- package/scripts/check-typecheck-tests.mjs +229 -72
- package/scripts/lib/dependency-tree.mjs +136 -0
- package/typescript/copy-contents/.husky/pre-push +45 -0
|
@@ -346,7 +346,7 @@ session found 13 of 13 issues filed this way, none of them through Lisa's
|
|
|
346
346
|
filing path. The rule saying not to do this already existed — it did not bind,
|
|
347
347
|
so it is now enforced here.
|
|
348
348
|
|
|
349
|
-
FILE IT THE SANCTIONED WAY — one of these
|
|
349
|
+
FILE IT THE SANCTIONED WAY — one of these three, always explicit:
|
|
350
350
|
|
|
351
351
|
1. The item is complete enough to build. Use the filing flow, not the CLI:
|
|
352
352
|
|
|
@@ -368,11 +368,26 @@ FILE IT THE SANCTIONED WAY — one of these two, always explicit:
|
|
|
368
368
|
Held for a human product call: <reason>.
|
|
369
369
|
<!-- [lisa-human-gate] reason=<short-slug> -->
|
|
370
370
|
|
|
371
|
+
3. The item is a CONTAINER — an Epic, or any item whose state rolls up from
|
|
372
|
+
its children. It is neither of the two above and must not pretend to be:
|
|
373
|
+
\`leaf-only-lifecycle\` FORBIDS the build-ready role on a container, and a
|
|
374
|
+
human gate it does not have is a durable false hold. It declares the third
|
|
375
|
+
thing instead — the canonical container line, in place of a Target Backend
|
|
376
|
+
Environment:
|
|
377
|
+
|
|
378
|
+
## Target Backend Environment
|
|
379
|
+
|
|
380
|
+
None — container: state rolls up from children
|
|
381
|
+
|
|
382
|
+
That declaration is refused alongside a by-design leaf type
|
|
383
|
+
(\`type:Bug\` / \`type:Task\` / \`type:Sub-task\` / \`type:Improvement\`):
|
|
384
|
+
an item cannot be a container and a leaf at once.
|
|
385
|
+
|
|
371
386
|
Filed, not ready, and no \`human_gate\` is the incomplete-handoff case, and
|
|
372
387
|
\`build_ready: false\` with no reason is the same omission with a value
|
|
373
388
|
attached. See the \`ready-role-filing\` rule for the full contract.
|
|
374
389
|
|
|
375
|
-
If you must run the CLI directly, the command has to carry one of the
|
|
390
|
+
If you must run the CLI directly, the command has to carry one of the three
|
|
376
391
|
declarations itself:
|
|
377
392
|
|
|
378
393
|
$(declaration_hint)
|
|
@@ -380,6 +395,9 @@ $(declaration_hint)
|
|
|
380
395
|
Or a \`[lisa-human-gate]\` marker in the body it submits, when a human
|
|
381
396
|
product call really is pending.
|
|
382
397
|
|
|
398
|
+
Or, for a container, \`None — container: state rolls up from children\` in
|
|
399
|
+
that body, and no by-design leaf type on the command.
|
|
400
|
+
|
|
383
401
|
WHERE THE DECLARATION IS READ FROM: argv, the request payload — inline, in a
|
|
384
402
|
\`--data-binary @file\`, or piped in over stdin — and the contents of a script
|
|
385
403
|
this command runs. Moving the create into a file no longer moves it out of
|
|
@@ -528,6 +546,78 @@ UNPARSEABLE_CREATION = re.compile(
|
|
|
528
546
|
|
|
529
547
|
BODY_FILE_FLAGS = {"--body-file", "-F", "--input", "--data-binary"}
|
|
530
548
|
LABEL_FLAGS = {"--label", "--labels", "--add-label", "--status", "--state"}
|
|
549
|
+
|
|
550
|
+
# ---------------------------------------------------------------------------
|
|
551
|
+
# THE THIRD DECLARATION: THIS ITEM IS A CONTAINER
|
|
552
|
+
#
|
|
553
|
+
# The two declarations above are both illegitimate for a container, and the
|
|
554
|
+
# rules say so in as many words. `leaf-only-lifecycle` FORBIDS the build-ready
|
|
555
|
+
# role on an Epic — its state rolls up from its children, and a hand-applied
|
|
556
|
+
# role on a parent is the exact input that rule's claim-time arm exists to
|
|
557
|
+
# reject. `lisa-github-write-issue` says a container needs no human gate.
|
|
558
|
+
# So a guard with only those two arms could be satisfied for a container only
|
|
559
|
+
# by writing something untrue, and BOTH untruths corrupt data another control
|
|
560
|
+
# reads: a stamped role puts a container in the build lane, and a fabricated
|
|
561
|
+
# hold marker is a state change with no inverse that reads to every later
|
|
562
|
+
# observer as a real product hold. A guard that can only be obeyed by lying is
|
|
563
|
+
# worse than one that simply refuses, because the lie is indistinguishable from
|
|
564
|
+
# a correct filing afterwards.
|
|
565
|
+
#
|
|
566
|
+
# WHY NOT `--label type:Epic`
|
|
567
|
+
#
|
|
568
|
+
# Because a declared type is a CLAIM, and one that costs a leaf nothing: the
|
|
569
|
+
# item it produces still looks buildable, so the exemption would be a free
|
|
570
|
+
# bypass of the readiness requirement for anything willing to mistype itself.
|
|
571
|
+
# That is the shape of this repository's measured case where an allowlist added
|
|
572
|
+
# to harden a guard became the way around it.
|
|
573
|
+
#
|
|
574
|
+
# WHAT IS READ INSTEAD
|
|
575
|
+
#
|
|
576
|
+
# The canonical container declaration the `derived-branch-plan` rule already
|
|
577
|
+
# defines and `lisa-github-write-issue` already stamps, in place of a Target
|
|
578
|
+
# Backend Environment:
|
|
579
|
+
#
|
|
580
|
+
# None — container: state rolls up from children
|
|
581
|
+
#
|
|
582
|
+
# It is a marker with no other meaning, so it is matched wherever the payload
|
|
583
|
+
# reaches — exactly the asymmetry `HUMAN_GATE_MARKER` gets, and for the same
|
|
584
|
+
# reason. Reading the declaration the writer already emits is also what keeps
|
|
585
|
+
# the guard and the skill from drifting apart a second time: there is one
|
|
586
|
+
# string, defined in one rule, and both read it.
|
|
587
|
+
#
|
|
588
|
+
# It is self-limiting in a way a type label is not. Writing it costs the item
|
|
589
|
+
# its Target Backend Environment and its Branch Plan — the two fields a leaf
|
|
590
|
+
# needs to be built at all — so the declaration is only useful to something
|
|
591
|
+
# that actually is a container. A leaf that writes it does not smuggle a
|
|
592
|
+
# buildable item past the gate; it files an unbuildable one, in the lane
|
|
593
|
+
# `leaf-only-lifecycle` and `lisa-repair-intake` already sweep.
|
|
594
|
+
#
|
|
595
|
+
# The WHOLE canonical value, `None` included, not just the distinctive tail.
|
|
596
|
+
# Dropping the leading half would match ordinary prose about how a container's
|
|
597
|
+
# state behaves — including a bug report ABOUT this guard — and a declaration
|
|
598
|
+
# that ordinary prose satisfies is not a declaration. Whitespace-tolerant
|
|
599
|
+
# because a body file may wrap the line; dash-tolerant because an author
|
|
600
|
+
# retyping it by hand will not always reach for the em dash.
|
|
601
|
+
CONTAINER_DECLARATION = re.compile(
|
|
602
|
+
r"None\s*[\u2014\u2013-]\s*container:\s*state\s+rolls\s+up\s+from\s+children",
|
|
603
|
+
re.IGNORECASE,
|
|
604
|
+
)
|
|
605
|
+
# The checkable half of "is it really a container". Provenance is unobservable
|
|
606
|
+
# here, as the header notes, but a CONTRADICTION is not: an item cannot be a
|
|
607
|
+
# container and a by-design leaf at once. Quoted from `leaf-only-lifecycle` —
|
|
608
|
+
# "the by-design leaf types (Bug, Task, Sub-task, Improvement)". Story and
|
|
609
|
+
# Spike are deliberately ABSENT: the same rule's childless-parent exception
|
|
610
|
+
# makes them leaf-or-container depending on child work, and a decomposition
|
|
611
|
+
# legitimately files a parent Story before the children that make it one.
|
|
612
|
+
BY_DESIGN_LEAF_TYPES = {"bug", "task", "sub-task", "subtask", "improvement"}
|
|
613
|
+
# Where a declared type can land on the created item. The label spellings are
|
|
614
|
+
# GitHub's `type:<value>` convention; `--type` and its aliases are the JIRA and
|
|
615
|
+
# Linear clients' spelling. Read with the same positional discipline as the
|
|
616
|
+
# build-ready role — a type named in a title or body is not a type applied.
|
|
617
|
+
TYPE_FLAGS = {
|
|
618
|
+
"--label", "--labels", "--add-label",
|
|
619
|
+
"--type", "--issue-type", "--issuetype",
|
|
620
|
+
}
|
|
531
621
|
POST_METHOD_FLAGS = {"-X", "--request", "--method"}
|
|
532
622
|
POST_PAYLOAD_FLAGS = {
|
|
533
623
|
"-d", "--data", "--data-raw", "--data-binary",
|
|
@@ -1433,6 +1523,65 @@ def declares_readiness(raw_args, roles, extra="", state_role_ok=False):
|
|
|
1433
1523
|
return False
|
|
1434
1524
|
|
|
1435
1525
|
|
|
1526
|
+
def declares_leaf_type(args):
|
|
1527
|
+
"""Whether the filing declares a by-design leaf type as a flag value.
|
|
1528
|
+
|
|
1529
|
+
Read only from positions that reach the created item, and never from a
|
|
1530
|
+
title or body — the same discipline the build-ready role is held to, for
|
|
1531
|
+
the same reason: a type named in prose is not a type applied.
|
|
1532
|
+
|
|
1533
|
+
Args:
|
|
1534
|
+
args: The creating command's arguments, already scoped to the flags.
|
|
1535
|
+
|
|
1536
|
+
Returns:
|
|
1537
|
+
True when a Bug / Task / Sub-task / Improvement is declared.
|
|
1538
|
+
"""
|
|
1539
|
+
for raw in flag_values(args, TYPE_FLAGS):
|
|
1540
|
+
for part in raw.split(","):
|
|
1541
|
+
value = part.strip().strip("'\"").lower()
|
|
1542
|
+
if value.startswith("type:"):
|
|
1543
|
+
value = value[len("type:") :]
|
|
1544
|
+
if value in BY_DESIGN_LEAF_TYPES:
|
|
1545
|
+
return True
|
|
1546
|
+
return False
|
|
1547
|
+
|
|
1548
|
+
|
|
1549
|
+
def declares_container(raw_args, texts=()):
|
|
1550
|
+
"""Whether the create declares the item a container.
|
|
1551
|
+
|
|
1552
|
+
A container is neither build-ready nor human-gated by construction, so this
|
|
1553
|
+
is the third accepted declaration rather than a third way to satisfy the
|
|
1554
|
+
first two. See the CONTAINER_DECLARATION block for why it reads the
|
|
1555
|
+
canonical declaration and not a `type:Epic` claim.
|
|
1556
|
+
|
|
1557
|
+
Args:
|
|
1558
|
+
raw_args: The creating command's arguments.
|
|
1559
|
+
texts: Payload or script text this command submits or runs.
|
|
1560
|
+
|
|
1561
|
+
Returns:
|
|
1562
|
+
True when the container declaration is present and uncontradicted.
|
|
1563
|
+
"""
|
|
1564
|
+
args = before_end_of_options(raw_args)
|
|
1565
|
+
# The contradiction check comes FIRST, so a filing that declares itself
|
|
1566
|
+
# both a container and a by-design leaf is refused rather than allowed on
|
|
1567
|
+
# the strength of the half that suits it.
|
|
1568
|
+
if declares_leaf_type(args):
|
|
1569
|
+
return False
|
|
1570
|
+
if CONTAINER_DECLARATION.search(" ".join(args)):
|
|
1571
|
+
return True
|
|
1572
|
+
for path in body_file_paths(args):
|
|
1573
|
+
try:
|
|
1574
|
+
with open(path, encoding="utf-8", errors="replace") as handle:
|
|
1575
|
+
if CONTAINER_DECLARATION.search(handle.read()):
|
|
1576
|
+
return True
|
|
1577
|
+
except OSError:
|
|
1578
|
+
continue
|
|
1579
|
+
for text in texts:
|
|
1580
|
+
if text and CONTAINER_DECLARATION.search(text):
|
|
1581
|
+
return True
|
|
1582
|
+
return False
|
|
1583
|
+
|
|
1584
|
+
|
|
1436
1585
|
def flag_values(args, names):
|
|
1437
1586
|
"""Every value assigned to one of the named flags.
|
|
1438
1587
|
|
|
@@ -1880,6 +2029,15 @@ def scan(text, depth, from_file=False):
|
|
|
1880
2029
|
state_role_ok = tracker in STATE_ROLE_TRACKERS and target is None
|
|
1881
2030
|
if declares_readiness(args, roles, submitted, state_role_ok):
|
|
1882
2031
|
continue
|
|
2032
|
+
# The container arm. Checked after readiness, so it adds a
|
|
2033
|
+
# compliant filing where none existed and takes none away where one
|
|
2034
|
+
# already did. `text` joins the scan only for a creation found
|
|
2035
|
+
# INSIDE a file, where the declaration lives in the same file as
|
|
2036
|
+
# the create it answers for.
|
|
2037
|
+
if declares_container(
|
|
2038
|
+
args, (submitted, text) if from_file else (submitted,)
|
|
2039
|
+
):
|
|
2040
|
+
continue
|
|
1883
2041
|
# `LIFECYCLE_ROLE=ready curl …` puts the declaration BEFORE the
|
|
1884
2042
|
# client, which is where an inline assignment has to go, so the
|
|
1885
2043
|
# role is read from the whole segment rather than from the client's
|
|
@@ -405,7 +405,12 @@ readonly FOLLOW_MAX_TOTAL_BYTES=524288
|
|
|
405
405
|
readonly FOLLOW_MAX_FILES=8
|
|
406
406
|
readonly FOLLOW_MAX_DEPTH=3
|
|
407
407
|
|
|
408
|
-
|
|
408
|
+
# Effective working directory established by a literal `cd` in the command, and
|
|
409
|
+
# the flag that says a `cd` happened whose target could NOT be read literally.
|
|
410
|
+
# Empty + 0 means no `cd` has been seen and the session's own cwd governs.
|
|
411
|
+
follow_cwd=""
|
|
412
|
+
follow_cwd_unknown=0
|
|
413
|
+
follow_normalized=""
|
|
409
414
|
follow_paths=$'\n'
|
|
410
415
|
follow_names=""
|
|
411
416
|
follow_count=0
|
|
@@ -420,6 +425,10 @@ see is the thing it exists to stop.
|
|
|
420
425
|
|
|
421
426
|
- Name the script by a LITERAL path that exists and is readable — \`bash
|
|
422
427
|
./scripts/x.sh\` rather than \`bash \"\$SCRIPT\"\` — and the guard can classify it.
|
|
428
|
+
- A leading \`cd\` the guard cannot read literally — \`cd \"\$DIR\" && bash x.sh\` —
|
|
429
|
+
leaves it unable to say WHICH copy of \`x.sh\` would run, and a same-named file
|
|
430
|
+
in another directory is a different file. Spell the directory out, or give the
|
|
431
|
+
script its full path.
|
|
423
432
|
- A file past the inspection cap is refused rather than half-scanned, because a
|
|
424
433
|
truncated scan reports a confident ALLOW about text it never read. Split it,
|
|
425
434
|
or run it manually outside the agent.
|
|
@@ -550,15 +559,77 @@ follow_split() {
|
|
|
550
559
|
'
|
|
551
560
|
}
|
|
552
561
|
|
|
553
|
-
#
|
|
554
|
-
#
|
|
555
|
-
|
|
556
|
-
|
|
562
|
+
# Collapse `.`, `..` and empty segments so the path a verdict NAMES is the one a
|
|
563
|
+
# reader recognises. Purely lexical, and it cannot move a verdict: every
|
|
564
|
+
# component has already been proven to exist by the `-d` test that follows, and
|
|
565
|
+
# the `-f`/`-r` tests are applied to the collapsed path either way.
|
|
566
|
+
follow_normalize() {
|
|
567
|
+
local p="$1" out="" seg
|
|
568
|
+
local IFS=/
|
|
569
|
+
set -f
|
|
570
|
+
# The word split is the point: `$p` is being addressed one path segment at a
|
|
571
|
+
# time, and the glob guard above keeps a literal `*` from expanding.
|
|
572
|
+
# shellcheck disable=SC2086
|
|
573
|
+
for seg in $p; do
|
|
574
|
+
case "$seg" in
|
|
575
|
+
"" | .) ;;
|
|
576
|
+
..) out="${out%/*}" ;;
|
|
577
|
+
*) out="$out/$seg" ;;
|
|
578
|
+
esac
|
|
579
|
+
done
|
|
580
|
+
set +f
|
|
581
|
+
follow_normalized="${out:-/}"
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
# Track the working directory a literal `cd` establishes, so a relative executed
|
|
585
|
+
# path resolves against the directory the command will actually have.
|
|
586
|
+
#
|
|
587
|
+
# This REPLACES the session's own cwd rather than adding to it. It used to be an
|
|
588
|
+
# extra base appended AFTER `$PWD`, so `$PWD` always won and
|
|
589
|
+
# `cd <other-checkout> && bash scripts/x.sh` was classified against the session
|
|
590
|
+
# checkout's same-named copy. That is not a resolution failure — it succeeds,
|
|
591
|
+
# against a real and readable file, and the verdict is reported with full
|
|
592
|
+
# confidence about text nobody was about to run (CodySwannGT/lisa#3933). Two
|
|
593
|
+
# checkouts of one repository differ by exactly the work in flight, which is the
|
|
594
|
+
# entire point of the second one, so the copy that would RUN is the only copy
|
|
595
|
+
# whose verdict means anything. Wrong in both directions, and the expensive
|
|
596
|
+
# direction is silent: the dangerous line sits in the copy that would run and
|
|
597
|
+
# the guard reads the pristine one.
|
|
598
|
+
#
|
|
599
|
+
# A target this hook cannot turn into a literal directory marks the cwd UNKNOWN
|
|
600
|
+
# instead of leaving a stale one in place. `follow_locate` then refuses a
|
|
601
|
+
# relative token rather than guessing, which is the honest-refusal doctrine the
|
|
602
|
+
# rest of this block already follows: a confident scan of the wrong file is
|
|
603
|
+
# strictly worse than saying it could not tell.
|
|
604
|
+
follow_cd() {
|
|
605
|
+
local d="$1" home="${HOME:-}"
|
|
606
|
+
# The tilde arms are LITERAL text — the unexpanded spelling the hook was
|
|
607
|
+
# handed — for the reason follow_locate's arms document.
|
|
608
|
+
# shellcheck disable=SC2088
|
|
609
|
+
case "$d" in
|
|
610
|
+
'~') d="$home" ;;
|
|
611
|
+
'~/'*) d="$home/${d#'~/'}" ;;
|
|
612
|
+
esac
|
|
613
|
+
case "$d" in
|
|
614
|
+
"" | -* | *'$'* | *'*'* | *'?'* | *'`'* | *'{'* | *'['*)
|
|
615
|
+
follow_cwd=""
|
|
616
|
+
follow_cwd_unknown=1
|
|
617
|
+
return 0
|
|
618
|
+
;;
|
|
619
|
+
esac
|
|
557
620
|
case "$d" in
|
|
558
|
-
|
|
621
|
+
/*) ;;
|
|
622
|
+
*) d="${follow_cwd:-$PWD}/$d" ;;
|
|
559
623
|
esac
|
|
560
|
-
|
|
561
|
-
|
|
624
|
+
follow_normalize "$d"
|
|
625
|
+
d="$follow_normalized"
|
|
626
|
+
if [ -d "$d" ]; then
|
|
627
|
+
follow_cwd="$d"
|
|
628
|
+
follow_cwd_unknown=0
|
|
629
|
+
else
|
|
630
|
+
follow_cwd=""
|
|
631
|
+
follow_cwd_unknown=1
|
|
632
|
+
fi
|
|
562
633
|
}
|
|
563
634
|
|
|
564
635
|
# Resolve one executed-file operand to a readable regular file, or fail.
|
|
@@ -571,6 +642,7 @@ follow_add_base() {
|
|
|
571
642
|
# this hook cannot evaluate.
|
|
572
643
|
follow_locate() {
|
|
573
644
|
local token="$1" home="${HOME:-}" tmp="${TMPDIR:-/tmp}" proj="${CLAUDE_PROJECT_DIR:-$PWD}" base
|
|
645
|
+
local cwd="${follow_cwd:-$PWD}"
|
|
574
646
|
follow_located=""
|
|
575
647
|
tmp="${tmp%/}"
|
|
576
648
|
[ -n "$tmp" ] || tmp="/tmp"
|
|
@@ -584,8 +656,14 @@ follow_locate() {
|
|
|
584
656
|
'${TMPDIR}' | '${TMPDIR}'/*) token="$tmp${token#'${TMPDIR}'}" ;;
|
|
585
657
|
'$HOME' | '$HOME'/*) token="$home${token#'$HOME'}" ;;
|
|
586
658
|
'${HOME}' | '${HOME}'/*) token="$home${token#'${HOME}'}" ;;
|
|
587
|
-
'$PWD' | '$PWD'/*)
|
|
588
|
-
|
|
659
|
+
'$PWD' | '$PWD'/*)
|
|
660
|
+
[ "$follow_cwd_unknown" -eq 0 ] || return 1
|
|
661
|
+
token="$cwd${token#'$PWD'}"
|
|
662
|
+
;;
|
|
663
|
+
'${PWD}' | '${PWD}'/*)
|
|
664
|
+
[ "$follow_cwd_unknown" -eq 0 ] || return 1
|
|
665
|
+
token="$cwd${token#'${PWD}'}"
|
|
666
|
+
;;
|
|
589
667
|
'$CLAUDE_PROJECT_DIR' | '$CLAUDE_PROJECT_DIR'/*) token="$proj${token#'$CLAUDE_PROJECT_DIR'}" ;;
|
|
590
668
|
'${CLAUDE_PROJECT_DIR}' | '${CLAUDE_PROJECT_DIR}'/*) token="$proj${token#'${CLAUDE_PROJECT_DIR}'}" ;;
|
|
591
669
|
esac
|
|
@@ -597,13 +675,26 @@ follow_locate() {
|
|
|
597
675
|
return 0
|
|
598
676
|
;;
|
|
599
677
|
esac
|
|
678
|
+
# Past this point the token is RELATIVE, so which directory it is resolved
|
|
679
|
+
# from decides which file the verdict is about.
|
|
680
|
+
if [ "$follow_cwd_unknown" -eq 1 ]; then
|
|
681
|
+
return 1
|
|
682
|
+
fi
|
|
683
|
+
if [ -n "$follow_cwd" ]; then
|
|
684
|
+
# A literal `cd` names the directory the shell will be in. It is the only
|
|
685
|
+
# candidate — falling back to `$PWD` here is what made the guard report
|
|
686
|
+
# confidently about a same-named file in another checkout.
|
|
687
|
+
[ -f "$follow_cwd/$token" ] && [ -r "$follow_cwd/$token" ] || return 1
|
|
688
|
+
follow_located="$follow_cwd/$token"
|
|
689
|
+
return 0
|
|
690
|
+
fi
|
|
600
691
|
while IFS= read -r base; do
|
|
601
692
|
[ -n "$base" ] || continue
|
|
602
693
|
if [ -f "$base/$token" ] && [ -r "$base/$token" ]; then
|
|
603
694
|
follow_located="$base/$token"
|
|
604
695
|
return 0
|
|
605
696
|
fi
|
|
606
|
-
done <<<"$PWD"$'\n'"$proj
|
|
697
|
+
done <<<"$PWD"$'\n'"$proj"
|
|
607
698
|
return 1
|
|
608
699
|
}
|
|
609
700
|
|
|
@@ -636,7 +727,15 @@ follow_take() {
|
|
|
636
727
|
follow_names="$follow_names $path"
|
|
637
728
|
followed_text="$followed_text"$'\n'"$content"
|
|
638
729
|
if [ "$depth" -lt "$FOLLOW_MAX_DEPTH" ]; then
|
|
730
|
+
# A `cd` inside the followed script governs what THAT script goes on to
|
|
731
|
+
# execute, and nothing after it in the command that ran the script — the
|
|
732
|
+
# child inherits this directory but cannot change the parent's. Saving and
|
|
733
|
+
# restoring is what keeps a nested `cd` from silently re-pointing the rest
|
|
734
|
+
# of the outer walk.
|
|
735
|
+
local saved_cwd="$follow_cwd" saved_unknown="$follow_cwd_unknown"
|
|
639
736
|
follow_scan "$content" "$((depth + 1))"
|
|
737
|
+
follow_cwd="$saved_cwd"
|
|
738
|
+
follow_cwd_unknown="$saved_unknown"
|
|
640
739
|
fi
|
|
641
740
|
}
|
|
642
741
|
|
|
@@ -861,7 +960,11 @@ follow_scan() {
|
|
|
861
960
|
cd)
|
|
862
961
|
if [ $((i + 1)) -lt "$n" ]; then
|
|
863
962
|
follow_unwrap "${toks[i + 1]}"
|
|
864
|
-
|
|
963
|
+
follow_cd "$follow_unwrapped"
|
|
964
|
+
else
|
|
965
|
+
# A bare `cd` is `cd "$HOME"`, and leaving the previous directory in
|
|
966
|
+
# place would resolve later tokens from a directory the shell left.
|
|
967
|
+
follow_cd "${HOME:-}"
|
|
865
968
|
fi
|
|
866
969
|
cmd_pos=0
|
|
867
970
|
i=$((i + 1))
|
|
@@ -289,6 +289,22 @@ proceed — the marker still holds. Never file the label *instead of* the marker
|
|
|
289
289
|
|
|
290
290
|
If a leaf arrives with `build_ready` omitted or `false` **and** no `human_gate`, do not create it: report the incomplete handoff and name both ways to resolve it (`build_ready: true`, or a `human_gate` reason). Containers are exempt — their state rolls up from children, so they need neither.
|
|
291
291
|
|
|
292
|
+
**The container exemption is executable, not prose.** It used to live only in the sentence above,
|
|
293
|
+
which meant the `block-direct-issue-create.sh` guard — the control that actually runs on the CLI
|
|
294
|
+
path — had no arm for it, and a container could satisfy that guard only by declaring a build-ready
|
|
295
|
+
role `leaf-only-lifecycle` forbids on it or a human gate it does not have. A container now declares
|
|
296
|
+
a **third** thing, and this skill already writes it: the canonical Target Backend Environment value
|
|
297
|
+
`None — container: state rolls up from children`, defined once by `derived-branch-plan`. The guard
|
|
298
|
+
reads that one string, so there is nothing for the skill and the guard to disagree about.
|
|
299
|
+
|
|
300
|
+
Two consequences for a container written from here. Render the Target Backend Environment section
|
|
301
|
+
with that exact value — an absent section reads as *underivable*, not exempt, and leaves the guard
|
|
302
|
+
nothing to read. And do not put a **by-design leaf type** (`type:Bug` / `type:Task` /
|
|
303
|
+
`type:Sub-task` / `type:Improvement`) on a filing carrying that declaration: an item cannot be a
|
|
304
|
+
container and a leaf at once, and the guard refuses the combination. `type:Story` and `type:Spike`
|
|
305
|
+
are not in that set, because `leaf-only-lifecycle`'s childless-parent exception makes them
|
|
306
|
+
leaf-or-container depending on child work.
|
|
307
|
+
|
|
292
308
|
## Phase 5.5 — Validate (Pre-write Gate)
|
|
293
309
|
|
|
294
310
|
Before any write, invoke `lisa-github-validate-issue` with the full proposed spec assembled from Phases 2 / 3 / 4 / 5. Pass it as a YAML block per the `lisa-github-validate-issue` schema, including `runtime_behavior_change`, `authenticated_surface`, and `artifacts_attached` flags so the right gates run.
|