loki-mode 7.129.5 → 8.0.1
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/README.md +121 -9
- package/SKILL.md +66 -5
- package/VERSION +1 -1
- package/autonomy/app-runner.sh +37 -0
- package/autonomy/completion-council.sh +862 -28
- package/autonomy/council-v2.sh +39 -0
- package/autonomy/crash.sh +3 -2
- package/autonomy/grill.sh +42 -0
- package/autonomy/hooks/validate-bash.sh +301 -4
- package/autonomy/lib/cockpit-render.sh +8 -2
- package/autonomy/lib/cr-rematerialize.py +101 -4
- package/autonomy/lib/deadline.py +957 -0
- package/autonomy/lib/dependency-setup.sh +205 -0
- package/autonomy/lib/done-recognition.sh +45 -0
- package/autonomy/lib/efficiency_cost.py +13 -6
- package/autonomy/lib/no_mock_scan.py +793 -0
- package/autonomy/lib/prd-enrich.sh +42 -0
- package/autonomy/lib/proof-analytics-props.py +69 -0
- package/autonomy/lib/proof-generator.py +326 -92
- package/autonomy/lib/proof-template.html +15 -12
- package/autonomy/lib/proof-verify.py +169 -105
- package/autonomy/lib/requirements_contract.py +848 -0
- package/autonomy/lib/sdk-mode.sh +103 -0
- package/autonomy/lib/secret-scan.sh +139 -0
- package/autonomy/lib/spec-expand.sh +208 -0
- package/autonomy/lib/tree_digest.py +266 -0
- package/autonomy/lib/voter-agents.sh +58 -8
- package/autonomy/lib/workspace_diff.py +141 -0
- package/autonomy/loki +196 -17
- package/autonomy/playwright-verify.sh +501 -0
- package/autonomy/prd-checklist.sh +17 -8
- package/autonomy/provider-offer.sh +111 -0
- package/autonomy/run.sh +4587 -671
- package/autonomy/sandbox.sh +42 -0
- package/autonomy/spec-interrogation.sh +148 -5
- package/autonomy/spec.sh +118 -1
- package/autonomy/telemetry.sh +133 -7
- package/autonomy/verify.sh +107 -0
- package/bin/loki +110 -3
- package/completions/_loki +10 -0
- package/completions/loki.bash +1 -1
- package/dashboard/__init__.py +1 -1
- package/dashboard/audit.py +96 -7
- package/dashboard/build_supervisor.py +1619 -0
- package/dashboard/control.py +8 -0
- package/dashboard/prompt_optimizer.py +7 -0
- package/dashboard/server.py +577 -22
- package/dashboard/static/trust.html +1 -1
- package/docs/ARCHITECTURE-OVERVIEW.md +207 -0
- package/docs/AUTONOMI-ECOSYSTEM.md +107 -0
- package/docs/INSTALLATION.md +19 -2
- package/docs/MODEL-EQUIVALENCE-HARNESS-PLAN.md +70 -0
- package/docs/PLANS-INDEX.md +33 -0
- package/docs/PRIVACY.md +29 -1
- package/docs/SIGNED-RECEIPTS.md +102 -0
- package/docs/SONNET5-DEFAULT-PLAN.md +1 -1
- package/docs/V8-ACCEPTANCE-TRIAGE-2026-07-24.md +114 -0
- package/docs/V8-AGENT-SDK-PLAN.md +692 -0
- package/docs/V8-COMPLEXITY-AUDIT-2026-07-24.md +45 -0
- package/docs/V8-MAJOR-RELEASE-PLAN.md +137 -0
- package/docs/V8-OVERNIGHT-PLAN-2026-07-25.md +82 -0
- package/docs/V8-RUNTIME-TRUTH-2026-07-25.md +232 -0
- package/docs/V8-SDK-RESEARCH-RAW.md +129 -0
- package/docs/alternative-installations.md +4 -4
- package/loki-ts/dist/loki.js +674 -285
- package/loki-ts/package.json +2 -0
- package/mcp/__init__.py +1 -1
- package/package.json +7 -6
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/providers/claude.sh +75 -0
- package/providers/codex.sh +85 -13
- package/references/sdk-mode.md +106 -0
- package/skills/model-selection.md +1 -1
- package/skills/providers.md +42 -0
- package/skills/quality-gates.md +22 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# sdk-mode.sh -- v8.1 one-switch SDK activation resolver (bash side).
|
|
3
|
+
#
|
|
4
|
+
# v8 gave every claude judge/text site and the RARV loop an SDK path, each behind
|
|
5
|
+
# its own LOKI_SDK_* flag. This adds ONE operator switch, LOKI_SDK_MODE, that sets
|
|
6
|
+
# the default for all of them at once, so an operator flips the whole engine to the
|
|
7
|
+
# SDK without learning 8 flags.
|
|
8
|
+
#
|
|
9
|
+
# LOKI_SDK_MODE=off (default) -- writes NOTHING; every site resolves to its
|
|
10
|
+
# own :-0 default. Byte-identical to v8. INV-OFF.
|
|
11
|
+
# LOKI_SDK_MODE=judges -- turn on the 7 one-shot JUDGE/TEXT sites; the
|
|
12
|
+
# RARV loop stays OFF (a behavior change we do
|
|
13
|
+
# NOT ship default this release). Recommended
|
|
14
|
+
# scale tier.
|
|
15
|
+
# LOKI_SDK_MODE=full -- all 8 on, including the RARV loop (LOKI_SDK_LOOP).
|
|
16
|
+
# Pre-flight tier; the loop default-flip is a
|
|
17
|
+
# later release.
|
|
18
|
+
# LOKI_SDK=1 is an alias for LOKI_SDK_MODE=judges; LOKI_SDK=full == full.
|
|
19
|
+
#
|
|
20
|
+
# DESIGN (write-once, zero call-site edits): every existing site reads its flag AT
|
|
21
|
+
# CALL TIME as `[ "${LOKI_SDK_X:-0}" = "1" ]` (bash) / `=== "1"` (TS). So we only
|
|
22
|
+
# need to EXPORT the resolved "0"/"1" into the env ONCE, before the loop runs, and
|
|
23
|
+
# every existing read honors it. We write a var ONLY when it is UNSET (`+set`
|
|
24
|
+
# guard), so an explicit per-site flag ALWAYS wins over the mode (INV-OVERRIDE) --
|
|
25
|
+
# even an explicit `LOKI_SDK_X=0` under `LOKI_SDK_MODE=full`.
|
|
26
|
+
#
|
|
27
|
+
# Call loki_sdk_resolve_mode ONCE, early, after this file is sourced and before any
|
|
28
|
+
# judge lib or the main loop runs.
|
|
29
|
+
#
|
|
30
|
+
# No emojis. No em dashes. bash 3.2 safe.
|
|
31
|
+
|
|
32
|
+
# The 8 activation vars, in a stable order. LOKI_SDK_LOOP is last (loop tier).
|
|
33
|
+
# Keep this list in sync with loki-ts/src/runner/sdk_mode.ts (parity fixture).
|
|
34
|
+
_LOKI_SDK_JUDGE_VARS="LOKI_SDK_DONE_RECOG LOKI_SDK_COUNCIL_V2 LOKI_SDK_CODE_REVIEW LOKI_SDK_COUNCIL_VOTE LOKI_SDK_VOTER_AGENTS LOKI_SDK_GRILL LOKI_SDK_PRD_ENRICH"
|
|
35
|
+
_LOKI_SDK_LOOP_VAR="LOKI_SDK_LOOP"
|
|
36
|
+
|
|
37
|
+
# Trim leading/trailing whitespace AND lowercase a value. Kept identical to the
|
|
38
|
+
# TS side's `.trim().toLowerCase()` (sdk_mode.ts) so bash and TS canonicalize a
|
|
39
|
+
# padded env var THE SAME WAY -- an operator who sets `LOKI_SDK_MODE=" full "`
|
|
40
|
+
# (trailing space from a YAML/.env/CI var) must get the same tier on both routes,
|
|
41
|
+
# not off-on-bash / full-on-TS (a real INV-PARITY split the shared fixture cannot
|
|
42
|
+
# catch). bash 3.2 safe: the extglob-free sed strips surrounding [:space:].
|
|
43
|
+
_loki_sdk_trim_lower() {
|
|
44
|
+
printf '%s' "$1" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//' | tr '[:upper:]' '[:lower:]'
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
# Normalize LOKI_SDK / LOKI_SDK_MODE to a canonical mode: off | judges | full.
|
|
48
|
+
_loki_sdk_canonical_mode() {
|
|
49
|
+
local mode
|
|
50
|
+
mode="$(_loki_sdk_trim_lower "${LOKI_SDK_MODE:-}")"
|
|
51
|
+
# LOKI_SDK alias (only when LOKI_SDK_MODE is empty/unset): "1"/"on"/"judges"
|
|
52
|
+
# -> judges; "full" -> full; else ignored.
|
|
53
|
+
if [ -z "$mode" ] && [ -n "${LOKI_SDK:-}" ]; then
|
|
54
|
+
case "$(_loki_sdk_trim_lower "$LOKI_SDK")" in
|
|
55
|
+
1|on|true|yes|judges) mode="judges" ;;
|
|
56
|
+
full) mode="full" ;;
|
|
57
|
+
0|off|false|no) mode="off" ;;
|
|
58
|
+
*) mode="" ;;
|
|
59
|
+
esac
|
|
60
|
+
fi
|
|
61
|
+
case "$mode" in
|
|
62
|
+
judges) printf 'judges' ;;
|
|
63
|
+
full) printf 'full' ;;
|
|
64
|
+
off|"") printf 'off' ;;
|
|
65
|
+
*) printf 'off' ;; # unknown value -> safe default (off), never on
|
|
66
|
+
esac
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
# Write $2 into var $1 ONLY when $1 is unset (so an explicit per-site flag wins).
|
|
70
|
+
_loki_sdk_default_if_unset() {
|
|
71
|
+
local var="$1" val="$2"
|
|
72
|
+
# `${!var+set}` is empty only when $var is UNSET (not when it is set-empty).
|
|
73
|
+
eval "local _cur=\"\${$var+set}\""
|
|
74
|
+
if [ -z "$_cur" ]; then
|
|
75
|
+
export "$var=$val"
|
|
76
|
+
fi
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
# The resolver. Idempotent + safe to call more than once.
|
|
80
|
+
loki_sdk_resolve_mode() {
|
|
81
|
+
local mode
|
|
82
|
+
mode="$(_loki_sdk_canonical_mode)"
|
|
83
|
+
[ "$mode" = "off" ] && return 0 # INV-OFF: write nothing.
|
|
84
|
+
|
|
85
|
+
local v
|
|
86
|
+
for v in $_LOKI_SDK_JUDGE_VARS; do
|
|
87
|
+
_loki_sdk_default_if_unset "$v" 1
|
|
88
|
+
done
|
|
89
|
+
if [ "$mode" = "full" ]; then
|
|
90
|
+
_loki_sdk_default_if_unset "$_LOKI_SDK_LOOP_VAR" 1
|
|
91
|
+
else
|
|
92
|
+
# judges tier: the loop stays OFF unless the operator set it explicitly.
|
|
93
|
+
_loki_sdk_default_if_unset "$_LOKI_SDK_LOOP_VAR" 0
|
|
94
|
+
fi
|
|
95
|
+
# Observability (opt-in): fanning one switch to 8 vars is action-at-a-distance;
|
|
96
|
+
# a debug operator should be able to see what got turned on. Off by default so
|
|
97
|
+
# INV-OFF and normal runs stay silent. Mirrored by sdk_mode.ts.
|
|
98
|
+
if [ "${LOKI_SDK_MODE_DEBUG:-0}" = "1" ]; then
|
|
99
|
+
printf 'loki: SDK mode=%s -> judges on, loop=%s\n' \
|
|
100
|
+
"$mode" "${LOKI_SDK_LOOP:-0}" >&2
|
|
101
|
+
fi
|
|
102
|
+
return 0
|
|
103
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# secret-scan.sh -- RUN-25 iter 21 (Wave D #2): the two-tier secret matcher,
|
|
3
|
+
# extracted to ONE sourceable location so the commit-time gate (autonomy/run.sh)
|
|
4
|
+
# and the completion EVIDENCE gate (autonomy/completion-council.sh) share a
|
|
5
|
+
# single implementation. Previously the matcher lived only in run.sh and ran only
|
|
6
|
+
# on the auto-commit path, so a completion-promise / dirty-tree / non-commit route
|
|
7
|
+
# could ship a leaked credential in a "done" deliverable -- a hole in the exact
|
|
8
|
+
# trust moat Loki sells. Both functions are pure (take a file path); no external
|
|
9
|
+
# state. Patterns are kept in lockstep with autonomy/verify.sh verify_secret_scan_file.
|
|
10
|
+
#
|
|
11
|
+
# No emojis. No em dashes.
|
|
12
|
+
|
|
13
|
+
_commit_scan_secret_file() {
|
|
14
|
+
# Two-tier secret matcher. Returns 0 if a high-confidence secret is found in
|
|
15
|
+
# the file, 1 otherwise. Patterns copied verbatim from the shipped scanner
|
|
16
|
+
# (autonomy/verify.sh verify_secret_scan_file) so the commit-time gate matches
|
|
17
|
+
# the verification gate's behavior. Top-level (not nested) so tests can
|
|
18
|
+
# override it for the mutation/non-vacuity proof.
|
|
19
|
+
local file="${1:-}"
|
|
20
|
+
[ -n "$file" ] && [ -f "$file" ] || return 1
|
|
21
|
+
|
|
22
|
+
# TEMPLATE / FIXTURE PATHS: a file whose NAME declares it is an example is
|
|
23
|
+
# not a leak, it is documentation. `.env.example` shipping
|
|
24
|
+
# `OPENAI_API_KEY=sk-...` is the correct way to show the expected shape, and
|
|
25
|
+
# generated apps produce these routinely. Without this, the completion gate
|
|
26
|
+
# blocks a finished, correct app for shipping its own template -- and the
|
|
27
|
+
# tier1 matcher below deliberately has no deny filter, so the usual
|
|
28
|
+
# example/placeholder escape hatch never gets a chance to apply.
|
|
29
|
+
# Scoped narrowly to paths that ANNOUNCE themselves: *.example, *.sample,
|
|
30
|
+
# *.template, *.dist, and files under a fixtures/ directory. A real secret
|
|
31
|
+
# in a real path is still caught by both tiers.
|
|
32
|
+
case "$file" in
|
|
33
|
+
*.example|*.example.*|*.sample|*.sample.*|*.template|*.template.*|*.dist|*/fixtures/*|*/__fixtures__/*)
|
|
34
|
+
return 1 ;;
|
|
35
|
+
esac
|
|
36
|
+
|
|
37
|
+
# TIER 1: specific formats. No deny filter -- a format match is a finding.
|
|
38
|
+
local tier1=(
|
|
39
|
+
'AKIA[0-9A-Z]{16}' # AWS access key id
|
|
40
|
+
'ASIA[0-9A-Z]{16}' # AWS temporary (STS) key id
|
|
41
|
+
'-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----' # PEM private key block
|
|
42
|
+
'gh[pousr]_[A-Za-z0-9]{36,}' # GitHub token (ghp_/gho_/...)
|
|
43
|
+
'github_pat_[A-Za-z0-9_]{60,}' # GitHub fine-grained PAT
|
|
44
|
+
'xox[baprs]-[A-Za-z0-9-]{10,}' # Slack token (xoxb-/xoxp-/...)
|
|
45
|
+
'sk-[A-Za-z0-9]{20,}' # OpenAI-style secret key
|
|
46
|
+
'AIza[0-9A-Za-z_-]{35}' # Google API key
|
|
47
|
+
'glpat-[A-Za-z0-9_-]{20,}' # GitLab personal access token
|
|
48
|
+
)
|
|
49
|
+
local p
|
|
50
|
+
for p in "${tier1[@]}"; do
|
|
51
|
+
# -e terminates option parsing so a pattern beginning with '-' (the PEM
|
|
52
|
+
# block) is not mistaken for a flag.
|
|
53
|
+
if LC_ALL=C grep -Eq -e "$p" "$file" 2>/dev/null; then
|
|
54
|
+
return 0
|
|
55
|
+
fi
|
|
56
|
+
done
|
|
57
|
+
|
|
58
|
+
# Deny filter for TIER 2: a matched line is IGNORED if it is plainly a
|
|
59
|
+
# placeholder or an environment-variable reference rather than a literal.
|
|
60
|
+
local deny='(\$\{|\$[A-Za-z_]|process\.env|os\.(environ|getenv)|%[A-Za-z_]+%|your[-_]|redacted|changeme|change[-_]me|placeholder|example|dummy|sample|fake|<[^>]*>|x{4,}|\*{4,})'
|
|
61
|
+
|
|
62
|
+
# TIER 2: generic assignments + bearer tokens + connection-string creds.
|
|
63
|
+
local tier2='(api[_-]?key|secret|token|password|passwd|access[_-]?key|client[_-]?secret|auth)[A-Za-z0-9_]*[[:space:]]*[:=][[:space:]]*["'"'"']?[A-Za-z0-9_/+.=-]{16,}'
|
|
64
|
+
local bearer='[Bb]earer[[:space:]]+[A-Za-z0-9_.\-]{20,}'
|
|
65
|
+
# URI-embedded credentials: scheme://user:password@host. The #1 leak vector
|
|
66
|
+
# in 12-factor apps (DATABASE_URL=postgres://u:pass@h, mongodb+srv://, redis://).
|
|
67
|
+
# Runs through the deny filter below, so ${VAR}-ref URIs are correctly ignored.
|
|
68
|
+
# Username segment is optional (*) so the password-only form redis://:pass@host
|
|
69
|
+
# (Redis < 6 / Heroku Redis / Redis Cloud emit exactly this) is caught too.
|
|
70
|
+
local uricred='[a-z][a-z0-9+.\-]*://[^/[:space:]:@]*:[^/[:space:]:@]+@'
|
|
71
|
+
|
|
72
|
+
local surviving
|
|
73
|
+
surviving="$(LC_ALL=C grep -EiI "$tier2|$bearer|$uricred" "$file" 2>/dev/null \
|
|
74
|
+
| LC_ALL=C grep -Eiv "$deny" 2>/dev/null)"
|
|
75
|
+
if [ -n "$surviving" ]; then
|
|
76
|
+
return 0
|
|
77
|
+
fi
|
|
78
|
+
return 1
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
_commit_path_looks_secret() {
|
|
82
|
+
# Filename/path heuristic. Returns 0 if the path looks like a credential or
|
|
83
|
+
# secret file ANYWHERE in the tree (basename OR any directory component),
|
|
84
|
+
# 1 otherwise. This is the PRIMARY commit-time guard: it catches likely-secret
|
|
85
|
+
# files regardless of where they sit and regardless of how weak the value
|
|
86
|
+
# inside looks, closing the nested-path gap that a top-level glob (':!credentials*')
|
|
87
|
+
# and a content-pattern scan both miss (e.g. secrets/credentials.json holding
|
|
88
|
+
# {"key":"sk-secret"}). The content scan (_commit_scan_secret_file) remains the
|
|
89
|
+
# complementary layer 2 for strong secrets hiding in non-obvious filenames.
|
|
90
|
+
#
|
|
91
|
+
# Safe-default bias: this runs only for the session-end AUTO-commit. A false
|
|
92
|
+
# positive merely leaves the file uncommitted for the user to commit by hand,
|
|
93
|
+
# which is acceptable and honest. So we err toward caution.
|
|
94
|
+
#
|
|
95
|
+
# Top-level (not nested) so tests can override it for the non-vacuity proof.
|
|
96
|
+
local p="${1:-}"
|
|
97
|
+
[ -n "$p" ] || return 1
|
|
98
|
+
# Case-insensitive match: lower the full path AND the basename, test both.
|
|
99
|
+
local lower base
|
|
100
|
+
lower="$(printf '%s' "$p" | tr '[:upper:]' '[:lower:]')"
|
|
101
|
+
base="${lower##*/}"
|
|
102
|
+
local cand
|
|
103
|
+
for cand in "$lower" "$base"; do
|
|
104
|
+
case "$cand" in
|
|
105
|
+
# dotenv files (basename or any path component ending in them)
|
|
106
|
+
.env|.env.*|*/.env|*/.env.*|*.env) return 0 ;;
|
|
107
|
+
# credential(s) anywhere (basename or any segment): secrets/credentials.json,
|
|
108
|
+
# aws-credentials, my-credential.txt, .git-credentials
|
|
109
|
+
*credential*) return 0 ;;
|
|
110
|
+
# a "secret"/"secrets" segment anywhere: secrets/anything, config/secret.json
|
|
111
|
+
*secret*) return 0 ;;
|
|
112
|
+
# private-key / keystore / cert material (extension-anchored so we do
|
|
113
|
+
# NOT match innocuous names like config.js or monkey.js)
|
|
114
|
+
*.pem|*.key|*.p12|*.keystore|*.pfx|*.jks|*.ppk) return 0 ;;
|
|
115
|
+
id_rsa|id_rsa.*|*/id_rsa|*/id_rsa.*) return 0 ;;
|
|
116
|
+
id_ed25519*|*/id_ed25519*) return 0 ;;
|
|
117
|
+
# token files: extension (*.token) OR "token" as a whole word/segment
|
|
118
|
+
# (delimited by /, -, _, or .). Deliberately NOT a bare *token*: that
|
|
119
|
+
# would flag ubiquitous innocuous frontend/parser names (tokenizer.js,
|
|
120
|
+
# tokens.css, design-tokens.json), and since the scan aborts the WHOLE
|
|
121
|
+
# session auto-commit on any single offender, one such file would block
|
|
122
|
+
# committing all of the user's work. Segment-style still catches real
|
|
123
|
+
# token files: api.token, auth_token, id-token, github.token, oauth-token.json.
|
|
124
|
+
*.token) return 0 ;;
|
|
125
|
+
token|token.*|token-*|token_*) return 0 ;;
|
|
126
|
+
*-token|*_token|*.token.*) return 0 ;;
|
|
127
|
+
*-token.*|*_token.*|*/token|*/token.*) return 0 ;;
|
|
128
|
+
*-token-*|*_token_*|*-token_*|*_token-*) return 0 ;;
|
|
129
|
+
# package/registry/cloud credential configs
|
|
130
|
+
.npmrc|*/.npmrc|.pypirc|*/.pypirc|.netrc|*/.netrc) return 0 ;;
|
|
131
|
+
*.kubeconfig|kubeconfig|*/kubeconfig) return 0 ;;
|
|
132
|
+
.dockercfg|*/.dockercfg|.docker/config.json|*/.docker/config.json) return 0 ;;
|
|
133
|
+
# service-account / gcp key json
|
|
134
|
+
service-account*.json|*/service-account*.json|*serviceaccount*) return 0 ;;
|
|
135
|
+
gcp-key*.json|*/gcp-key*.json) return 0 ;;
|
|
136
|
+
esac
|
|
137
|
+
done
|
|
138
|
+
return 1
|
|
139
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# spec-expand.sh -- RUN-25 iter 24 (Wave D #4): turn an API CONTRACT (OpenAPI /
|
|
3
|
+
# GraphQL SDL / Postman collection) passed as the build source into a full,
|
|
4
|
+
# per-operation build checklist instead of letting the raw contract get copied
|
|
5
|
+
# verbatim and then TRUNCATED to the first 4000 bytes of the prompt (a real
|
|
6
|
+
# defect: a 40-operation OpenAPI file loses 21 of 40 ops to `head -c 4000`, so
|
|
7
|
+
# the builder never sees over half the contract).
|
|
8
|
+
#
|
|
9
|
+
# One entry point: spec_maybe_expand_contract <src-file>
|
|
10
|
+
# - sniffs the file; if it is an OpenAPI/GraphQL/Postman contract, expands it
|
|
11
|
+
# into .loki/generated-prd.md as a checklist (one `- [ ]` per operation,
|
|
12
|
+
# keyed by operationId/field/request name) and echoes ".loki/generated-prd.md".
|
|
13
|
+
# - otherwise echoes NOTHING and touches no state (the caller keeps the raw
|
|
14
|
+
# path -> existing behavior is byte-identical for non-contract specs).
|
|
15
|
+
#
|
|
16
|
+
# Deterministic, pure-python stdlib (json always; yaml when importable, regex
|
|
17
|
+
# fallback otherwise). No new dependency, no network, no LLM. Not byte-lock
|
|
18
|
+
# gated: it rewrites an INPUT file, it does not change build_prompt() output.
|
|
19
|
+
#
|
|
20
|
+
# No emojis. No em dashes.
|
|
21
|
+
|
|
22
|
+
# spec_maybe_expand_contract <src>
|
|
23
|
+
# On a recognized contract, writes the per-operation checklist to a TEMP file and
|
|
24
|
+
# echoes that temp path (the caller feeds it to persist_user_prd, which copies it
|
|
25
|
+
# into .loki/generated-prd.md AND writes the source:"user" signature so a later
|
|
26
|
+
# no-file run reuses it exactly like a user PRD -- no duplicated persist logic).
|
|
27
|
+
# Echoes empty and touches no state on anything that is not a contract.
|
|
28
|
+
#
|
|
29
|
+
# For a standalone caller (no persist_user_prd in scope, e.g. the unit test), set
|
|
30
|
+
# LOKI_SPEC_EXPAND_INPLACE=1 to write straight to .loki/generated-prd.md and echo
|
|
31
|
+
# that path instead. The default (temp + persist) is what the runner uses.
|
|
32
|
+
spec_maybe_expand_contract() {
|
|
33
|
+
local src="$1"
|
|
34
|
+
[ -n "$src" ] || { echo ""; return 0; }
|
|
35
|
+
[ -f "$src" ] || { echo ""; return 0; }
|
|
36
|
+
command -v python3 >/dev/null 2>&1 || { echo ""; return 0; }
|
|
37
|
+
|
|
38
|
+
# Never re-expand our own generated PRD.
|
|
39
|
+
case "$src" in
|
|
40
|
+
*.loki/generated-prd.md|*.loki/generated-prd.json) echo ""; return 0 ;;
|
|
41
|
+
esac
|
|
42
|
+
|
|
43
|
+
local loki_dir="${TARGET_DIR:-.}/.loki"
|
|
44
|
+
local inplace="${LOKI_SPEC_EXPAND_INPLACE:-0}"
|
|
45
|
+
# Default: write a temp checklist the caller persists (writes the signature).
|
|
46
|
+
# In-place: write the canonical slot directly (standalone/test path).
|
|
47
|
+
local tmp dest
|
|
48
|
+
if [ "$inplace" = "1" ]; then
|
|
49
|
+
mkdir -p "$loki_dir" 2>/dev/null || { echo ""; return 0; }
|
|
50
|
+
dest="$loki_dir/generated-prd.md"
|
|
51
|
+
tmp="$loki_dir/.generated-prd.md.expand.$$"
|
|
52
|
+
else
|
|
53
|
+
tmp="${TMPDIR:-/tmp}/loki-spec-expand.$$.md"
|
|
54
|
+
dest="$tmp"
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
# The python does the sniff + expansion. It writes the checklist to $tmp and
|
|
58
|
+
# exits 0 ONLY when it actually recognized a contract; any other exit (not a
|
|
59
|
+
# contract, parse error, empty operation set) leaves state untouched.
|
|
60
|
+
if _SPEC_EXP_SRC="$src" _SPEC_EXP_OUT="$tmp" python3 - <<'PYEOF'
|
|
61
|
+
import json, os, re, sys
|
|
62
|
+
|
|
63
|
+
src = os.environ["_SPEC_EXP_SRC"]
|
|
64
|
+
out = os.environ["_SPEC_EXP_OUT"]
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
with open(src, encoding="utf-8", errors="replace") as fh:
|
|
68
|
+
raw = fh.read()
|
|
69
|
+
except OSError:
|
|
70
|
+
sys.exit(2)
|
|
71
|
+
|
|
72
|
+
if not raw.strip():
|
|
73
|
+
sys.exit(2)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def load_structured(text):
|
|
77
|
+
# Try JSON first (Postman + OpenAPI-json), then YAML if available.
|
|
78
|
+
try:
|
|
79
|
+
return json.loads(text)
|
|
80
|
+
except Exception:
|
|
81
|
+
pass
|
|
82
|
+
try:
|
|
83
|
+
import yaml # PyYAML present in this env; optional elsewhere.
|
|
84
|
+
return yaml.safe_load(text)
|
|
85
|
+
except Exception:
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def norm_id(text, fallback):
|
|
90
|
+
t = (text or "").strip()
|
|
91
|
+
if not t:
|
|
92
|
+
return fallback
|
|
93
|
+
t = re.sub(r"\s+", " ", t)
|
|
94
|
+
return t
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
data = load_structured(raw)
|
|
98
|
+
ops = [] # list of (op_id, description)
|
|
99
|
+
kind = None
|
|
100
|
+
|
|
101
|
+
# ---- OpenAPI / Swagger (structured) ----------------------------------------
|
|
102
|
+
if isinstance(data, dict) and ("openapi" in data or "swagger" in data) and isinstance(data.get("paths"), dict):
|
|
103
|
+
kind = "OpenAPI"
|
|
104
|
+
for path, item in data["paths"].items():
|
|
105
|
+
if not isinstance(item, dict):
|
|
106
|
+
continue
|
|
107
|
+
for method, op in item.items():
|
|
108
|
+
if method.lower() not in ("get", "post", "put", "patch", "delete", "head", "options", "trace"):
|
|
109
|
+
continue
|
|
110
|
+
if not isinstance(op, dict):
|
|
111
|
+
continue
|
|
112
|
+
opid = op.get("operationId") or "%s %s" % (method.upper(), path)
|
|
113
|
+
summary = op.get("summary") or op.get("description") or ""
|
|
114
|
+
desc = "%s %s -- %s" % (method.upper(), path, summary.strip()) if summary else "%s %s" % (method.upper(), path)
|
|
115
|
+
ops.append((norm_id(opid, "%s %s" % (method.upper(), path)), desc.strip()))
|
|
116
|
+
|
|
117
|
+
# ---- Postman collection (structured) ---------------------------------------
|
|
118
|
+
elif isinstance(data, dict) and isinstance(data.get("info"), dict) and "item" in data:
|
|
119
|
+
kind = "Postman"
|
|
120
|
+
|
|
121
|
+
def walk(items, prefix=""):
|
|
122
|
+
for it in items or []:
|
|
123
|
+
if not isinstance(it, dict):
|
|
124
|
+
continue
|
|
125
|
+
name = it.get("name", "request")
|
|
126
|
+
if isinstance(it.get("item"), list): # folder
|
|
127
|
+
walk(it["item"], prefix + name + " / ")
|
|
128
|
+
elif isinstance(it.get("request"), (dict, str)):
|
|
129
|
+
req = it["request"]
|
|
130
|
+
method = req.get("method", "") if isinstance(req, dict) else ""
|
|
131
|
+
label = (prefix + name).strip()
|
|
132
|
+
ops.append((label, ("%s %s" % (method, label)).strip()))
|
|
133
|
+
|
|
134
|
+
walk(data.get("item"))
|
|
135
|
+
|
|
136
|
+
# ---- GraphQL SDL (text; not JSON/YAML) -------------------------------------
|
|
137
|
+
if not ops:
|
|
138
|
+
# Recognize a schema with a Query/Mutation/Subscription type and pull each
|
|
139
|
+
# field as one operation. Cheap brace-depth scan, no graphql lib needed.
|
|
140
|
+
if re.search(r"\btype\s+(Query|Mutation|Subscription)\b", raw):
|
|
141
|
+
kind = "GraphQL"
|
|
142
|
+
for root in ("Query", "Mutation", "Subscription"):
|
|
143
|
+
m = re.search(r"\btype\s+" + root + r"\b[^{]*\{", raw)
|
|
144
|
+
if not m:
|
|
145
|
+
continue
|
|
146
|
+
i = m.end()
|
|
147
|
+
depth = 1
|
|
148
|
+
body_start = i
|
|
149
|
+
while i < len(raw) and depth > 0:
|
|
150
|
+
if raw[i] == "{":
|
|
151
|
+
depth += 1
|
|
152
|
+
elif raw[i] == "}":
|
|
153
|
+
depth -= 1
|
|
154
|
+
i += 1
|
|
155
|
+
body = raw[body_start:i - 1]
|
|
156
|
+
# A field line: name(optional args): ReturnType
|
|
157
|
+
for fm in re.finditer(r"^\s*([A-Za-z_]\w*)\s*(\([^)]*\))?\s*:\s*([^\n#]+)", body, re.M):
|
|
158
|
+
field = fm.group(1)
|
|
159
|
+
ret = fm.group(3).strip()
|
|
160
|
+
ops.append(("%s.%s" % (root, field), "%s %s -> %s" % (root, field, ret)))
|
|
161
|
+
|
|
162
|
+
if not ops:
|
|
163
|
+
sys.exit(3) # nothing recognized -> caller keeps the raw path
|
|
164
|
+
|
|
165
|
+
# De-dup on op id, preserve order.
|
|
166
|
+
seen = set()
|
|
167
|
+
uniq = []
|
|
168
|
+
for oid, desc in ops:
|
|
169
|
+
if oid in seen:
|
|
170
|
+
continue
|
|
171
|
+
seen.add(oid)
|
|
172
|
+
uniq.append((oid, desc))
|
|
173
|
+
|
|
174
|
+
title = os.path.basename(src)
|
|
175
|
+
lines = []
|
|
176
|
+
lines.append("# Build checklist from %s contract: %s" % (kind, title))
|
|
177
|
+
lines.append("")
|
|
178
|
+
lines.append("Source: %s (%s). Each operation below is one build requirement." % (title, kind))
|
|
179
|
+
lines.append("Implement, test, and verify every item. Do not drop any operation.")
|
|
180
|
+
lines.append("")
|
|
181
|
+
lines.append("## Operations (%d)" % len(uniq))
|
|
182
|
+
lines.append("")
|
|
183
|
+
for oid, desc in uniq:
|
|
184
|
+
lines.append("- [ ] %s: %s" % (oid, desc))
|
|
185
|
+
lines.append("")
|
|
186
|
+
|
|
187
|
+
try:
|
|
188
|
+
with open(out, "w", encoding="utf-8") as fh:
|
|
189
|
+
fh.write("\n".join(lines))
|
|
190
|
+
except OSError:
|
|
191
|
+
sys.exit(4)
|
|
192
|
+
|
|
193
|
+
sys.exit(0)
|
|
194
|
+
PYEOF
|
|
195
|
+
then
|
|
196
|
+
if [ "$inplace" = "1" ]; then
|
|
197
|
+
mv -f "$tmp" "$dest" 2>/dev/null || { rm -f "$tmp" 2>/dev/null; echo ""; return 0; }
|
|
198
|
+
echo ".loki/generated-prd.md"
|
|
199
|
+
else
|
|
200
|
+
# tmp == dest here; hand the temp checklist to the caller to persist.
|
|
201
|
+
echo "$tmp"
|
|
202
|
+
fi
|
|
203
|
+
return 0
|
|
204
|
+
fi
|
|
205
|
+
rm -f "$tmp" 2>/dev/null
|
|
206
|
+
echo ""
|
|
207
|
+
return 0
|
|
208
|
+
}
|