@givolt/i3-composer-agent 3.1.0 → 3.2.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.
Files changed (86) hide show
  1. package/.agents/plugins/marketplace.json +1 -1
  2. package/.claude/commands/i3-sop.md +3 -3
  3. package/.claude-plugin/marketplace.json +1 -1
  4. package/.claude-plugin/plugin.json +1 -1
  5. package/.codex-plugin/plugin.json +1 -1
  6. package/.gemini/commands/i3-sop.toml +3 -3
  7. package/README.md +22 -27
  8. package/commands/i3-sop.toml +3 -3
  9. package/docs/codex-setup.md +1 -1
  10. package/docs/other-hosts.md +1 -1
  11. package/docs/workbuddy-setup.md +1 -1
  12. package/evals/cases/i3-enterprise-sop.json +70 -32
  13. package/evals/zh-CN/cases/i3-enterprise-sop.json +77 -31
  14. package/package.json +1 -23
  15. package/plugin.json +1 -1
  16. package/skills/i3-enterprise-sop/SKILL.md +151 -91
  17. package/skills/i3-enterprise-sop/references/engineering-skeleton/directory-standard.md +29 -15
  18. package/skills/i3-enterprise-sop/references/handoff.py +171 -100
  19. package/skills/i3-enterprise-sop/references/read_text.py +60 -0
  20. package/skills/i3-enterprise-sop/references/technical-baseline/baselines/enterprise-application-technical-baseline-v0-0-4.md +28 -28
  21. package/skills/i3-enterprise-sop/references/technical-baseline/readme.md +9 -118
  22. package/skills/i3-enterprise-sop/references/{ad/templates → templates}/ad.md +34 -27
  23. package/skills/i3-enterprise-sop/references/{brd/templates → templates}/brd.md +21 -10
  24. package/skills/i3-enterprise-sop/references/{prd/templates → templates}/prd.md +57 -24
  25. package/skills/i3-enterprise-sop/references/{testcase/templates → templates}/testcase.md +32 -22
  26. package/docs/upstream-repository/AGENTS.md +0 -92
  27. package/docs/upstream-repository/CLAUDE.md +0 -60
  28. package/docs/upstream-repository/CONTRIBUTING.md +0 -137
  29. package/docs/upstream-repository/README.md +0 -8
  30. package/docs/upstream-repository/gitattributes.txt +0 -1
  31. package/docs/upstream-repository/gitignore.txt +0 -10
  32. package/docs/upstream-repository/manifest.json +0 -71
  33. package/docs/upstream-repository/opencode-skills.link +0 -1
  34. package/docs/upstream-repository/skill-gap.yml +0 -80
  35. package/docs/upstream-repository/skills-contributing.md +0 -15
  36. package/docs/upstream-repository/test-plugin-install.yml +0 -167
  37. package/skills/i3-enterprise-sop/references/ad/assets-manifest.json +0 -86
  38. package/skills/i3-enterprise-sop/references/ad/prompts/01-generate.md +0 -58
  39. package/skills/i3-enterprise-sop/references/ad/prompts/02-review.md +0 -54
  40. package/skills/i3-enterprise-sop/references/ad/prompts/03-revise.md +0 -54
  41. package/skills/i3-enterprise-sop/references/ad/prompts/04-recheck.md +0 -54
  42. package/skills/i3-enterprise-sop/references/ad/readme.md +0 -70
  43. package/skills/i3-enterprise-sop/references/assets-manifest.json +0 -230
  44. package/skills/i3-enterprise-sop/references/brd/assets-manifest.json +0 -83
  45. package/skills/i3-enterprise-sop/references/brd/prompts/01-generate.md +0 -51
  46. package/skills/i3-enterprise-sop/references/brd/prompts/02-review.md +0 -47
  47. package/skills/i3-enterprise-sop/references/brd/prompts/03-revise.md +0 -47
  48. package/skills/i3-enterprise-sop/references/brd/prompts/04-recheck.md +0 -47
  49. package/skills/i3-enterprise-sop/references/brd/readme.md +0 -50
  50. package/skills/i3-enterprise-sop/references/codebuddy-project-guide.md +0 -34
  51. package/skills/i3-enterprise-sop/references/controlled-handoff.md +0 -41
  52. package/skills/i3-enterprise-sop/references/controlled_handoff.py +0 -379
  53. package/skills/i3-enterprise-sop/references/engineering-skeleton/adoption.md +0 -200
  54. package/skills/i3-enterprise-sop/references/engineering-skeleton/catalog.json +0 -17
  55. package/skills/i3-enterprise-sop/references/engineering-skeleton/readme.md +0 -52
  56. package/skills/i3-enterprise-sop/references/handoff-contract.md +0 -196
  57. package/skills/i3-enterprise-sop/references/lifecycle-contract.md +0 -91
  58. package/skills/i3-enterprise-sop/references/plan-handoff.md +0 -73
  59. package/skills/i3-enterprise-sop/references/prd/assets-manifest.json +0 -109
  60. package/skills/i3-enterprise-sop/references/prd/prompts/01-generate.md +0 -64
  61. package/skills/i3-enterprise-sop/references/prd/prompts/02-review.md +0 -49
  62. package/skills/i3-enterprise-sop/references/prd/prompts/03-revise.md +0 -48
  63. package/skills/i3-enterprise-sop/references/prd/prompts/04-recheck.md +0 -47
  64. package/skills/i3-enterprise-sop/references/prd/readme.md +0 -137
  65. package/skills/i3-enterprise-sop/references/prd/template-contracts.md +0 -39
  66. package/skills/i3-enterprise-sop/references/prd/templates/ad.md +0 -17
  67. package/skills/i3-enterprise-sop/references/readme.md +0 -167
  68. package/skills/i3-enterprise-sop/references/recovery-contract.md +0 -65
  69. package/skills/i3-enterprise-sop/references/review-handoff.md +0 -105
  70. package/skills/i3-enterprise-sop/references/selection-handoff.md +0 -55
  71. package/skills/i3-enterprise-sop/references/selection_handoff.py +0 -225
  72. package/skills/i3-enterprise-sop/references/stages.json +0 -271
  73. package/skills/i3-enterprise-sop/references/technical-baseline/assets-manifest.json +0 -84
  74. package/skills/i3-enterprise-sop/references/template-contracts.md +0 -142
  75. package/skills/i3-enterprise-sop/references/testcase/assets-manifest.json +0 -91
  76. package/skills/i3-enterprise-sop/references/testcase/prompts/01-generate.md +0 -53
  77. package/skills/i3-enterprise-sop/references/testcase/prompts/02-review.md +0 -51
  78. package/skills/i3-enterprise-sop/references/testcase/prompts/03-revise.md +0 -49
  79. package/skills/i3-enterprise-sop/references/testcase/prompts/04-recheck.md +0 -51
  80. package/skills/i3-enterprise-sop/references/testcase/readme.md +0 -61
  81. package/skills/i3-enterprise-sop/references/verification/check_execution.py +0 -185
  82. package/skills/i3-enterprise-sop/references/verification/check_native_execution.py +0 -282
  83. package/skills/i3-enterprise-sop/references/verification/check_reads.py +0 -348
  84. package/skills/i3-enterprise-sop/references/verification/readme.md +0 -149
  85. package/skills/i3-enterprise-sop/references/verification/review-output.schema.json +0 -155
  86. package/skills/i3-enterprise-sop/references/verification/review_handoff.py +0 -329
@@ -1,185 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Read-only CodeBuddy stream-json execution/report check, version 0.1.0.
3
-
4
- The caller supplies the independently produced core audit and a fixed scope
5
- policy. This tool derives attempts from tool_use/tool_result, never from the
6
- model's description. It does not approve a business document or write a project.
7
- """
8
- import argparse
9
- from collections import Counter
10
- import json
11
- from pathlib import Path
12
- import re
13
- import shlex
14
- import sys
15
-
16
- START='<!-- i3-sop-execution-report -->'
17
- END='<!-- /i3-sop-execution-report -->'
18
- OPERATIONS=('help','init','select','resolve','other')
19
- STATUSES=('succeeded','blocked','failed','unconfirmed')
20
- BOUNDARY_FAILURES={'unexpected_project_file_changes','forbidden_or_outside_source_read','forbidden_tool_attempt','read_only_case_changed_files','read_only_case_created_i3_directory'}
21
-
22
-
23
- def strict_object(pairs):
24
- out={}
25
- for key,value in pairs:
26
- if key in out:raise ValueError('duplicate JSON key: '+key)
27
- out[key]=value
28
- return out
29
-
30
-
31
- def loads(text):
32
- return json.loads(text,object_pairs_hook=strict_object)
33
-
34
-
35
- def content(block):
36
- value=block.get('content','')
37
- if isinstance(value,str):return value
38
- if isinstance(value,list):return '\n'.join(x.get('text','') for x in value if isinstance(x,dict) and isinstance(x.get('text',''),str))
39
- return ''
40
-
41
-
42
- def command_scope(command,policy):
43
- """Accept one direct python3 invocation, with exact scope and named args."""
44
- try:tokens=shlex.split(command)
45
- except ValueError:return 'other',False
46
- if len(tokens)<3 or tokens[:2]!=['python3',policy['helper']]:return 'other',False
47
- if tokens[2:]==['--help']:return 'help',True
48
- action=tokens[2]
49
- if action not in ('init','select','resolve'):return 'other',False
50
- args=tokens[3:];options={}
51
- if len(args)%2:return action,False
52
- for key,value in zip(args[::2],args[1::2]):
53
- if not key.startswith('--') or key in options:return action,False
54
- options[key]=value
55
- base={'--project-root':policy['projectRoot'],'--manifest':policy['manifest']}
56
- if action=='init':allowed=options=={**base,'--work':policy['work']}
57
- elif action=='resolve':allowed=options=={**base,'--capability-id':policy['capabilityId'],'--prd-id':policy['prdId']}
58
- else:
59
- refs=policy['selections']
60
- allowed=any(options=={**base,'--role':ref['role'],'--path':ref['path'],
61
- '--capability-id':ref['capabilityId'],'--prd-id':ref['prdId']} for ref in refs)
62
- return action,allowed
63
-
64
-
65
- def outcome(result,operation):
66
- if result is None:return 'unconfirmed','missing_tool_result'
67
- text=content(result)
68
- if result.get('is_error') and re.search(r'permission[^\n]*(denied|not allowed)|not (?:been )?granted permission',text,re.I):
69
- return 'blocked','permission_denial'
70
- exit_codes=re.findall(r'^Exit Code:\s*(-?\d+)\s*$',text,re.M|re.I)
71
- if len(exit_codes)!=1:return ('failed','tool_error') if result.get('is_error') else ('unconfirmed','unsupported_result_carrier')
72
- if result.get('is_error') or int(exit_codes[0])!=0:return 'failed','tool_or_process_error'
73
- if operation in ('init','select','resolve'):
74
- match=re.search(r'^Stdout: (.*?)\n\s*Stderr:',text,re.M|re.S)
75
- try:value=loads(match[1]) if match else None
76
- except (ValueError,TypeError):value=None
77
- if not isinstance(value,dict) or type(value.get('ok')) is not bool:return 'unconfirmed','helper_result_not_proven'
78
- if not value['ok']:return 'failed','helper_reported_failure'
79
- return 'succeeded','returned_exit_zero'
80
-
81
-
82
- def attempts_from_trace(events,policy):
83
- calls=[];results={};ids=set();issues=[]
84
- for seq,event in enumerate(events):
85
- blocks=(event.get('message') or {}).get('content',[])
86
- if not isinstance(blocks,list):continue
87
- for block in blocks:
88
- if not isinstance(block,dict):continue
89
- if event.get('type')=='assistant' and block.get('type')=='tool_use':
90
- ident=block.get('id')
91
- if not isinstance(ident,str) or ident in ids:issues.append('missing_or_duplicate_tool_call_id')
92
- ids.add(ident);calls.append((seq,block))
93
- if event.get('type')=='user' and block.get('type')=='tool_result':
94
- ident=block.get('tool_use_id')
95
- if ident in results:issues.append('duplicate_tool_result')
96
- if ident not in ids:issues.append('result_without_prior_call')
97
- results[ident]=(seq,block)
98
- attempts=[]
99
- for seq,call in calls:
100
- name=call.get('name')
101
- if name in ('PowerShell','NotebookEdit','Write','Edit','Task','Agent'):issues.append('unsupported_or_forbidden_tool_attempt:'+name)
102
- if name!='Bash':continue
103
- command=(call.get('input') or {}).get('command','')
104
- if not isinstance(command,str):command='';issues.append('invalid_shell_command')
105
- op,allowed=command_scope(command,policy)
106
- returned=results.get(call.get('id'));status,reason=outcome(returned[1] if returned else None,op)
107
- attempts.append({'ordinal':len(attempts)+1,'toolCallId':call.get('id'),'traceLine':seq+1,
108
- 'resultLine':returned[0]+1 if returned else None,'command':command,'operation':op,
109
- 'allowed':allowed,'status':status,'statusEvidence':reason})
110
- return attempts,sorted(set(issues))
111
-
112
-
113
- def reporting_check(text,attempts,legacy_report):
114
- if legacy_report and START not in text and END not in text:
115
- return {'passed':False,'status':'not_checked_legacy','issues':['legacy_report_requires_separate_human_explanation']}
116
- errors=[]
117
- try:
118
- if text.count(START)!=1 or text.count(END)!=1:raise ValueError('exactly_one_report_block_required')
119
- begin=text.index(START)+len(START);end=text.index(END)
120
- block=text[begin:end].strip()
121
- match=re.fullmatch(r'```json\s*\n(.*?)\n```',block,re.S)
122
- if not match:raise ValueError('json_fence_required')
123
- data=loads(match[1])
124
- if not isinstance(data,dict) or set(data)!={'schemaVersion','shellAttempts'} or type(data['schemaVersion']) is not int or data['schemaVersion']!=1:
125
- raise ValueError('report_root_contract_mismatch')
126
- rows=data['shellAttempts']
127
- if not isinstance(rows,list) or len(rows)!=len(attempts):raise ValueError('attempt_count_mismatch')
128
- for actual,row in zip(attempts,rows):
129
- if not isinstance(row,dict) or set(row)!={'ordinal','operation','status'}:raise ValueError('attempt_row_contract_mismatch')
130
- if type(row['ordinal']) is not int or row['ordinal']!=actual['ordinal']:raise ValueError('attempt_ordinal_mismatch')
131
- if row['operation'] not in OPERATIONS or row['status'] not in STATUSES:raise ValueError('attempt_enum_mismatch')
132
- if row['operation']!=actual['operation'] or row['status']!=actual['status']:errors.append('attempt_evidence_mismatch:'+str(actual['ordinal']))
133
- except (ValueError,TypeError,KeyError) as exc:errors.append(str(exc))
134
- return {'passed':not errors,'status':'checked' if not errors else 'needs_review','issues':errors,
135
- 'scope':'structured attempts only; narrative, business adequacy and approval require human review'}
136
-
137
-
138
- def check(events,policy,core,report,legacy_report=False):
139
- attempts,issues=attempts_from_trace(events,policy)
140
- core_failures=core.get('failures')
141
- if not isinstance(core_failures,list) or type(core.get('passed')) is not bool:
142
- raise ValueError('independent_core_audit_contract_missing')
143
- functional=[x for x in core_failures if x not in BOUNDARY_FAILURES]
144
- boundary=[x for x in core_failures if x in BOUNDARY_FAILURES]
145
- if core.get('unexpectedChanges'):boundary.append('unexpected_project_file_changes')
146
- if core.get('forbiddenReads'):boundary.append('forbidden_or_outside_source_read')
147
- if core.get('allExistingProjectFilesUnchanged') is not True:boundary.append('existing_files_unchanged_not_proven')
148
- if not core['passed'] and not functional and not boundary:functional.append('core_audit_failed_without_details')
149
- discipline=list(issues)
150
- for row in attempts:
151
- if not row['allowed']:discipline.append('outside_allowed_command:'+str(row['ordinal']))
152
- if row['status']!='succeeded':discipline.append('non_success_attempt:'+str(row['ordinal']))
153
- if row['allowed'] and row['status']!='succeeded':functional.append('required_helper_not_successful:'+str(row['ordinal']))
154
- counts=Counter(x['operation'] for x in attempts if x['allowed'] and x['status']=='succeeded')
155
- for op,minimum in policy.get('requiredMinimum',{}).items():
156
- if op not in OPERATIONS or type(minimum) is not int or minimum<0:raise ValueError('invalid_required_operation')
157
- if counts[op]<minimum:functional.append('required_operation_missing:'+op)
158
- if not attempts or attempts[0]['operation']!='help' or not attempts[0]['allowed']:discipline.append('first_shell_must_be_exact_help')
159
- if not legacy_report and core.get('inputsReadyBeforeFirstShell') is not True:discipline.append('inputs_before_first_shell_not_proven')
160
- reporting=reporting_check(report,attempts,legacy_report)
161
- checks={'functional':{'passed':not functional,'issues':sorted(set(functional))},
162
- 'boundary':{'passed':not boundary,'issues':sorted(set(boundary))},
163
- 'execution':{'passed':not discipline,'issues':sorted(set(discipline))},'reporting':reporting}
164
- verdict='failed' if functional or boundary else 'needs_review' if discipline or not reporting['passed'] else 'checks_passed'
165
- return {'schemaVersion':1,'checkerVersion':'0.1.0','profile':'CodeBuddy stream-json/direct-python3-helper',
166
- 'checks':checks,'attempts':attempts,'verdict':verdict,'humanApproval':False,
167
- 'limits':['caller fixes the scope policy and independently verifies the core audit',
168
- 'not an OS sandbox, universal shell parser, business approval, Plan/Build acceptance or publication authorization']}
169
-
170
-
171
- def main():
172
- parser=argparse.ArgumentParser(description=__doc__)
173
- for name in ('trace','policy','core','report'):parser.add_argument('--'+name,required=True,type=Path)
174
- parser.add_argument('--legacy-report',action='store_true',help='retain old free-text format without retroactive format failure')
175
- args=parser.parse_args()
176
- try:
177
- events=[loads(line) for line in args.trace.read_text().splitlines() if line.strip()]
178
- result=check(events,loads(args.policy.read_text()),loads(args.core.read_text()),args.report.read_text(),args.legacy_report)
179
- except (OSError,ValueError,TypeError,KeyError) as exc:
180
- result={'schemaVersion':1,'checkerVersion':'0.1.0','verdict':'needs_review','humanApproval':False,'inputError':str(exc)}
181
- print(json.dumps(result,ensure_ascii=False,indent=2))
182
- return 0 if result.get('verdict')=='checks_passed' else 1
183
-
184
-
185
- if __name__=='__main__':sys.exit(main())
@@ -1,282 +0,0 @@
1
- #!/usr/bin/env python3
2
- """CodeBuddy native-method evidence/report audit, v0.1.1; read-only stdout.
3
-
4
- Separate from the direct-python helper profile in check_execution.py. A frozen
5
- caller scope defines evidence obligations; neither the model nor this checker
6
- creates business approval. Does not execute tools or modify project files.
7
- """
8
- import argparse
9
- import hashlib
10
- import json
11
- from pathlib import Path
12
- import re
13
- import stat
14
- import sys
15
-
16
- START = '<!-- i3-sop-native-execution-report -->'
17
- END = '<!-- /i3-sop-native-execution-report -->'
18
- PROFILES = ('generation', 'status', 'plan', 'auto-preflight', 'standard')
19
- GATE_STATES = ('not_applicable', 'not_checked', 'pending', 'blocked', 'satisfied')
20
- INPUT_STATES = ('full', 'partial', 'not_read')
21
-
22
-
23
- def unique_object(pairs):
24
- out = {}
25
- for key, value in pairs:
26
- if key in out:
27
- raise ValueError('duplicate_json_key:' + key)
28
- out[key] = value
29
- return out
30
-
31
-
32
- def loads(text):
33
- return json.loads(text, object_pairs_hook=unique_object,
34
- parse_constant=lambda value: (_ for _ in ()).throw(ValueError('invalid_constant:' + value)))
35
-
36
-
37
- def inventory(root):
38
- files = {}
39
- for p in Path(root).rglob('*'):
40
- if '.git' in p.relative_to(root).parts:
41
- continue
42
- if p.is_symlink():
43
- files[str(p.relative_to(root))] = {'symlink': str(p.readlink())}
44
- elif p.is_file():
45
- files[str(p.relative_to(root))] = {
46
- 'sha256': hashlib.sha256(p.read_bytes()).hexdigest(),
47
- 'mode': stat.S_IMODE(p.stat().st_mode)}
48
- return files
49
-
50
-
51
- def content(block):
52
- raw = block.get('content', '')
53
- return raw if isinstance(raw, str) else '\n'.join(
54
- row.get('text', '') for row in raw if isinstance(row, dict)) if isinstance(raw, list) else ''
55
-
56
-
57
- def outcome(result, tool):
58
- if result is None:
59
- return 'unconfirmed', 'missing_tool_result'
60
- text = content(result)
61
- error_carrier = result.get('is_error') or re.match(r'^\s*(?:Error:|Permission denied\b)', text)
62
- if error_carrier and re.search(r'permission[^\n]*(denied|not allowed)|not (?:been )?granted permission', text, re.I):
63
- return 'blocked', 'permission_denial'
64
- if result.get('is_error') or re.match(r'^\s*Error:', text):
65
- return 'failed', 'tool_flag_or_error_text'
66
- if tool == 'Bash':
67
- codes = re.findall(r'^Exit Code:\s*(-?\d+)\s*$', text, re.M | re.I)
68
- if len(codes) != 1:
69
- return 'unconfirmed', 'shell_exit_not_proven'
70
- if int(codes[0]) != 0:
71
- return 'failed', 'nonzero_process_exit'
72
- return 'succeeded', 'tool_receipt'
73
-
74
-
75
- def trace_calls(events):
76
- calls, results, issues = [], {}, []
77
- ids = set()
78
- for seq, event in enumerate(events):
79
- blocks = (event.get('message') or {}).get('content', [])
80
- if not isinstance(blocks, list):
81
- continue
82
- for block in blocks:
83
- if not isinstance(block, dict):
84
- continue
85
- if event.get('type') == 'assistant' and block.get('type') == 'tool_use':
86
- ident = block.get('id')
87
- if not isinstance(ident, str) or ident in ids:
88
- issues.append('missing_or_duplicate_tool_call_id')
89
- ids.add(ident)
90
- calls.append({**block, 'seq': seq})
91
- elif event.get('type') == 'user' and block.get('type') == 'tool_result':
92
- ident = block.get('tool_use_id')
93
- if ident not in ids or ident in results:
94
- issues.append('orphan_or_duplicate_tool_result')
95
- results[ident] = (seq, block)
96
- return calls, results, issues
97
-
98
-
99
- def report_block(text):
100
- if text.count(START) != 1 or text.count(END) != 1:
101
- raise ValueError('exactly_one_native_report_block_required')
102
- block = text.split(START, 1)[1].split(END, 1)[0].strip()
103
- match = re.fullmatch(r'```json\s*\n(.*?)\n```', block, re.S)
104
- if not match:
105
- raise ValueError('native_report_json_fence_required')
106
- value = loads(match[1])
107
- if not isinstance(value, dict) or set(value) != {'schemaVersion', 'failedAttempts', 'inputChecks', 'gates'}:
108
- raise ValueError('native_report_root_contract_mismatch')
109
- if type(value['schemaVersion']) is not int or value['schemaVersion'] != 1:
110
- raise ValueError('native_report_version_mismatch')
111
- for key, fields in [('failedAttempts', {'tool', 'subject', 'status'}),
112
- ('inputChecks', {'path', 'status'}), ('gates', {'id', 'status'})]:
113
- if not isinstance(value[key], list) or any(not isinstance(row, dict) or set(row) != fields for row in value[key]):
114
- raise ValueError('native_report_rows_invalid:' + key)
115
- if any(not isinstance(item, str) for row in value[key] for item in row.values()):
116
- raise ValueError('native_report_values_must_be_strings:' + key)
117
- if any(row['status'] not in ('blocked', 'failed', 'unconfirmed') for row in value['failedAttempts']):
118
- raise ValueError('native_attempt_state_invalid')
119
- if any(row['status'] not in INPUT_STATES for row in value['inputChecks']):
120
- raise ValueError('native_input_state_invalid')
121
- if any(row['status'] not in GATE_STATES for row in value['gates']):
122
- raise ValueError('native_gate_state_invalid')
123
- return value
124
-
125
-
126
- def assess(root, scope, events, process, legacy_report=False):
127
- root = Path(root).resolve()
128
- if scope.get('profile') not in PROFILES:
129
- raise ValueError('unknown_native_profile')
130
- for key in ('requiredReads', 'reportInputs', 'allowedWrites', 'requiredSkills', 'files', 'gateExpectations'):
131
- if key not in scope:
132
- raise ValueError('frozen_scope_missing:' + key)
133
- if any(path != '.codebuddy/.i3-composer-agent.json' for path in scope.get('requiredReadLines', {})):
134
- raise ValueError('selected_line_exception_is_locator_metadata_only')
135
- calls, results, trace_issues = trace_calls(events)
136
- attempts, reads, writes, methods, forbidden = [], [], [], [], []
137
- def relative(raw):
138
- p = Path(raw)
139
- p = (p if p.is_absolute() else root / p).resolve()
140
- return str(p.relative_to(root)) if p.is_relative_to(root) else str(p)
141
- for call in calls:
142
- args = call.get('input') or {}
143
- returned = results.get(call['id'])
144
- receipt = returned[1] if returned else None
145
- status, reason = outcome(receipt, call['name'])
146
- text = content(receipt or {})
147
- raw = args.get('file_path') or args.get('path')
148
- path = relative(raw) if isinstance(raw, str) else None
149
- tool = call['name']
150
- subject = args.get('skill', '') if tool == 'Skill' else args.get('command', '') if tool == 'Bash' else path or ''
151
- attempts.append({'ordinal': len(attempts) + 1, 'toolCallId': call['id'], 'traceLine': call['seq'] + 1,
152
- 'resultLine': returned[0] + 1 if returned else None, 'tool': tool, 'subject': subject,
153
- 'status': status, 'statusEvidence': reason})
154
- if path and (Path(path).is_absolute() or path in scope.get('forbiddenReads', [])
155
- or any(path == prefix or path.startswith(prefix.rstrip('/') + '/')
156
- for prefix in scope.get('forbiddenAccessPrefixes', []))):
157
- forbidden.append(path)
158
- if tool == 'Read' and status == 'succeeded':
159
- reads.append({'path': path, 'seq': returned[0], 'requestSeq': call['seq'],
160
- 'lines': {int(n): value for n, value in re.findall(r'^\s*(\d+)→(.*)$', text, re.M)}})
161
- if tool == 'Grep' and path in scope.get('requiredReadLines', {}) and status == 'succeeded':
162
- # This exception is only for locator metadata; caller explicitly freezes those lines.
163
- try:
164
- parts = loads(text)
165
- except ValueError:
166
- parts = text.splitlines()
167
- selected = {}
168
- for part in parts if isinstance(parts, list) else []:
169
- match = re.match(re.escape(str(root / path)) + r':(\d+):(.*)$', str(part))
170
- if match:
171
- selected[int(match[1])] = match[2]
172
- reads.append({'path': path, 'seq': returned[0], 'requestSeq': call['seq'], 'lines': selected})
173
- if tool in ('Write', 'Edit'):
174
- writes.append({'path': path, 'seq': call['seq'],
175
- 'receiptSeq': returned[0] if returned else float('inf'), 'status': status})
176
- if tool == 'Skill':
177
- local = root / '.codebuddy/skills' / str(subject) / 'SKILL.md'
178
- valid_name = bool(re.fullmatch(r'[a-z][a-z0-9-]*', str(subject)))
179
- body = local.read_text().split('---', 2)[-1].strip() if valid_name and local.is_file() else None
180
- methods.append({'name': subject, 'seq': returned[0] if returned else float('inf'), 'success': status == 'succeeded',
181
- 'fullLocalBody': bool(body and body in text and str(local.parent) in text)})
182
- first = min((row['seq'] for row in writes), default=float('inf'))
183
- def coverage(path, before=float('inf'), after=-1, selected=None):
184
- observed = {}
185
- for read in reads:
186
- if read['path'] == path and after < read['seq'] < before and (after < 0 or read['requestSeq'] > after):
187
- observed.update(read['lines'])
188
- p = root / path
189
- lines = p.read_text().splitlines() if p.is_file() else []
190
- wanted = list(selected) if selected is not None else list(range(1, len(lines) + 1))
191
- correct = [n for n in wanted if 1 <= n <= len(lines) and observed.get(n) == lines[n - 1]]
192
- state = 'full' if wanted and len(correct) == len(wanted) else 'partial' if observed else 'not_read'
193
- return {'path': path, 'status': state, 'requiredLines': len(wanted), 'matchedLines': len(correct)}
194
- required = [coverage(path, before=first, selected=scope.get('requiredReadLines', {}).get(path))
195
- for path in scope['requiredReads']]
196
- targets = []
197
- for path in scope['allowedWrites']:
198
- last = max((row['receiptSeq'] for row in writes if row['path'] == path and row['status'] == 'succeeded'), default=None)
199
- targets.append(coverage(path, after=last if last is not None else float('inf')))
200
- current = inventory(root)
201
- changed = sorted(name for name in set(current) | set(scope['files']) if current.get(name) != scope['files'].get(name))
202
- boundary_issues = ['unexpected_file_change:' + name for name in changed if name not in scope['allowedWrites']]
203
- boundary_issues += ['write_attempt_outside_scope:' + str(row['path']) for row in writes if row['path'] not in scope['allowedWrites']]
204
- boundary_issues += ['forbidden_or_outside_access:' + path for path in forbidden]
205
- discipline = list(trace_issues)
206
- allowed_tools = scope.get('tools', scope.get('allowedTools', []))
207
- for row in attempts:
208
- if row['tool'] not in allowed_tools:
209
- discipline.append('tool_outside_scope:' + row['tool'])
210
- if row['tool'] == 'Bash' and row['subject'] not in scope.get('allowedShellCommands', []):
211
- discipline.append('shell_command_outside_exact_scope:' + row['subject'])
212
- if row['status'] != 'succeeded':
213
- discipline.append('tool_attempt_' + row['status'] + ':' + str(row['ordinal']))
214
- method_issues = []
215
- for skill in scope['requiredSkills']:
216
- if not any(row['name'] == skill and row['success'] and row['fullLocalBody'] and row['seq'] < first for row in methods):
217
- method_issues.append('required_native_skill_not_loaded:' + skill)
218
- method_issues += ['forbidden_skill_loaded:' + row['name'] for row in methods if row['name'] in scope.get('forbiddenSkills', [])]
219
- finals = [event for event in events if event.get('type') == 'result']
220
- text = finals[-1].get('result', '') if finals else ''
221
- report_issues, gate_issues = [], []
222
- reporting_status = 'checked'
223
- if legacy_report and START not in text:
224
- reporting_status = 'not_checked_legacy'
225
- report_issues.append('old_native_report_preserved_no_retroactive_format_claim')
226
- else:
227
- try:
228
- report = report_block(text)
229
- actual_failed = [{key: row[key] for key in ('tool', 'subject', 'status')}
230
- for row in attempts if row['status'] != 'succeeded']
231
- if report['failedAttempts'] != actual_failed:
232
- report_issues.append('failed_attempts_do_not_match_trace')
233
- expected_inputs = [{'path': path, 'status': coverage(path, selected=scope.get('requiredReadLines', {}).get(path))['status']}
234
- for path in scope['reportInputs']]
235
- if sorted(report['inputChecks'], key=lambda row: row['path']) != sorted(expected_inputs, key=lambda row: row['path']):
236
- report_issues.append('input_claims_do_not_match_actual_reads')
237
- expected_gates = [{'id': key, 'status': value} for key, value in scope['gateExpectations'].items()]
238
- if sorted(report['gates'], key=lambda row: row['id']) != sorted(expected_gates, key=lambda row: row['id']):
239
- gate_issues.append('gate_claims_differ_from_frozen_expectations')
240
- except (ValueError, TypeError, KeyError) as exc:
241
- report_issues.append(str(exc))
242
- gate_issues.append('gate_report_not_validated')
243
- if attempts and re.search(r'(?:^|[;;])\s*(?:无\s*(?:Bash\s*[//、和]\s*)?工具调用|未调用任何工具)\s*(?:[。;;]|$)', text, re.M):
244
- report_issues.append('narrative_no_tools_claim_contradicts_trace')
245
- if any(row['tool'] == 'Bash' for row in attempts) and re.search(r'(?:^|[;;])\s*无\s*Bash(?:\s*[,,。;;]|\s*$)', text, re.M):
246
- report_issues.append('narrative_no_bash_claim_contradicts_trace')
247
- completed = bool(process.get('exitCode') == 0 and not process.get('timedOut') and finals and not finals[-1].get('is_error'))
248
- sections = {
249
- 'completion': {'passed': completed},
250
- 'inputs': {'passed': all(row['status'] == 'full' for row in required), 'checks': required},
251
- 'targets': {'passed': all(row['status'] == 'full' for row in targets), 'checks': targets},
252
- 'boundaries': {'passed': not boundary_issues, 'issues': boundary_issues, 'changedFiles': changed},
253
- 'nativeMethods': {'passed': not method_issues, 'issues': method_issues, 'loads': methods},
254
- 'toolDiscipline': {'passed': not discipline, 'issues': discipline},
255
- 'reportConsistency': {'passed': not report_issues, 'status': reporting_status, 'issues': report_issues},
256
- 'gateReport': {'passed': not gate_issues, 'issues': gate_issues,
257
- 'scope': 'matches caller test expectations; not human approval or semantic gate certification'}}
258
- return {'checkerVersion': '0.1.1', 'profile': scope['profile'],
259
- 'passed': all(section['passed'] for section in sections.values()), **sections, 'attempts': attempts,
260
- 'proofScope': 'literal reads, local method load, file/tool boundary and structured report evidence; narrative semantics require human review',
261
- 'businessApproved': False, 'implementationApproved': False, 'published': False}
262
-
263
-
264
- def main():
265
- parser = argparse.ArgumentParser(description=__doc__)
266
- for name in ('project-root', 'scope', 'trace', 'process'):
267
- parser.add_argument('--' + name, required=True)
268
- parser.add_argument('--legacy-report', action='store_true')
269
- args = parser.parse_args()
270
- try:
271
- scope = loads(Path(args.scope).read_text())
272
- events = [loads(line) for line in Path(args.trace).read_text().splitlines() if line.strip()]
273
- result = assess(Path(args.project_root), scope, events, loads(Path(args.process).read_text()), args.legacy_report)
274
- print(json.dumps(result, ensure_ascii=False, indent=2))
275
- return 0 if result['passed'] else 1
276
- except (OSError, ValueError, TypeError, KeyError) as exc:
277
- print(json.dumps({'passed': False, 'error': str(exc), 'businessApproved': False}, ensure_ascii=False))
278
- return 1
279
-
280
-
281
- if __name__ == '__main__':
282
- sys.exit(main())